mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			e680ef0263
			...
			7e03d960bd
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7e03d960bd | ||
| 
						 | 
					f76a3666b1 | ||
| 
						 | 
					d6971fb22f | ||
| 
						 | 
					796bf1c2dc | ||
| 
						 | 
					959ec0c424 | 
@@ -1,3 +1,5 @@
 | 
			
		||||
import { Command, createCli } from "@/lib/ccCLI";
 | 
			
		||||
import { Ok } from "@/lib/thirdparty/ts-result-es";
 | 
			
		||||
import { CCLog } from "@/lib/ccLog";
 | 
			
		||||
import {
 | 
			
		||||
  AccessConfig,
 | 
			
		||||
@@ -5,574 +7,382 @@ import {
 | 
			
		||||
  loadConfig,
 | 
			
		||||
  saveConfig,
 | 
			
		||||
} from "./config";
 | 
			
		||||
import { ChatBoxEvent, pullEventAs } from "@/lib/event";
 | 
			
		||||
import { parseBoolean } from "@/lib/common";
 | 
			
		||||
 | 
			
		||||
// CLI命令接口
 | 
			
		||||
interface CLICommand {
 | 
			
		||||
  name: string;
 | 
			
		||||
  description: string;
 | 
			
		||||
  usage: string;
 | 
			
		||||
  execute: (args: string[], executor: string, context: CLIContext) => CLIResult;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CLI执行结果
 | 
			
		||||
interface CLIResult {
 | 
			
		||||
  success: boolean;
 | 
			
		||||
  message?: string;
 | 
			
		||||
  shouldSaveConfig?: boolean;
 | 
			
		||||
  config?: AccessConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CLI上下文
 | 
			
		||||
interface CLIContext {
 | 
			
		||||
// 1. Define AppContext
 | 
			
		||||
export interface AppContext {
 | 
			
		||||
  configFilepath: string;
 | 
			
		||||
  reloadConfig: () => void;
 | 
			
		||||
  log: CCLog;
 | 
			
		||||
  chatBox: ChatBoxPeripheral;
 | 
			
		||||
  logger: CCLog;
 | 
			
		||||
  print: (
 | 
			
		||||
    message: string | MinecraftTextComponent | MinecraftTextComponent[],
 | 
			
		||||
  ) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getGroupNames(config: AccessConfig) {
 | 
			
		||||
  return config.usersGroups.flatMap((value) => value.groupName);
 | 
			
		||||
  return config.usersGroups.map((value) => value.groupName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 基础命令处理器
 | 
			
		||||
class CLICommandProcessor {
 | 
			
		||||
  private commands = new Map<string, CLICommand>();
 | 
			
		||||
  private context: CLIContext;
 | 
			
		||||
// 2. Define Commands
 | 
			
		||||
 | 
			
		||||
  constructor(context: CLIContext) {
 | 
			
		||||
    this.context = context;
 | 
			
		||||
    this.initializeCommands();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private initializeCommands() {
 | 
			
		||||
    // 注册所有命令
 | 
			
		||||
    this.registerCommand(new AddCommand());
 | 
			
		||||
    this.registerCommand(new DelCommand());
 | 
			
		||||
    this.registerCommand(new ListCommand());
 | 
			
		||||
    this.registerCommand(new SetCommand());
 | 
			
		||||
    this.registerCommand(new EditCommand());
 | 
			
		||||
    this.registerCommand(new ShowConfigCommand());
 | 
			
		||||
    this.registerCommand(new HelpCommand());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private registerCommand(command: CLICommand) {
 | 
			
		||||
    this.commands.set(command.name, command);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public processCommand(message: string, executor: string): CLIResult {
 | 
			
		||||
    const params = message.split(" ");
 | 
			
		||||
 | 
			
		||||
    // 移除 "@AC" 前缀
 | 
			
		||||
    if (params.length < 2) {
 | 
			
		||||
      return this.getHelpCommand().execute([], executor, this.context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const commandName = params[1].replace("/", ""); // 移除 "/" 前缀
 | 
			
		||||
    const args = params.slice(2);
 | 
			
		||||
 | 
			
		||||
    const command = this.commands.get(commandName);
 | 
			
		||||
    if (!command) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Unknown command: ${commandName}`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ret = command.execute(args, executor, this.context);
 | 
			
		||||
    return ret;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getHelpCommand(): CLICommand {
 | 
			
		||||
    return this.commands.get("help")!;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public sendResponse(result: CLIResult, executor: string) {
 | 
			
		||||
    if (result.message != null && result.message.length > 0) {
 | 
			
		||||
      this.context.chatBox.sendMessageToPlayer(
 | 
			
		||||
        result.message,
 | 
			
		||||
        executor,
 | 
			
		||||
        "AccessControl",
 | 
			
		||||
        "[]",
 | 
			
		||||
        undefined,
 | 
			
		||||
        undefined,
 | 
			
		||||
        true,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (result.shouldSaveConfig === true) {
 | 
			
		||||
      saveConfig(result.config!, this.context.configFilepath);
 | 
			
		||||
      this.context.reloadConfig();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加用户命令
 | 
			
		||||
class AddCommand implements CLICommand {
 | 
			
		||||
  name = "add";
 | 
			
		||||
  description = "Add player to group";
 | 
			
		||||
  usage = "add <userGroup> <playerName>";
 | 
			
		||||
 | 
			
		||||
  execute(args: string[], _executor: string, context: CLIContext): CLIResult {
 | 
			
		||||
    if (args.length !== 2) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Usage: ${this.usage}`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [groupName, playerName] = args;
 | 
			
		||||
    const config: AccessConfig = loadConfig(context.configFilepath)!;
 | 
			
		||||
 | 
			
		||||
    if (groupName === "admin") {
 | 
			
		||||
      config.adminGroupConfig.groupUsers.push(playerName);
 | 
			
		||||
      return {
 | 
			
		||||
        success: true,
 | 
			
		||||
        message: `Add player ${playerName} to admin`,
 | 
			
		||||
        shouldSaveConfig: true,
 | 
			
		||||
        config,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const groupNames = getGroupNames(config);
 | 
			
		||||
 | 
			
		||||
    if (!groupNames.includes(groupName)) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Invalid group: ${groupName}. Available groups: ${groupNames.join(
 | 
			
		||||
          ", ",
 | 
			
		||||
        )}`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const groupConfig = config.usersGroups.find(
 | 
			
		||||
      (value) => value.groupName === groupName,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!groupConfig) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Group ${groupName} not found`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (groupConfig.groupUsers === undefined) {
 | 
			
		||||
      groupConfig.groupUsers = [playerName];
 | 
			
		||||
    } else {
 | 
			
		||||
      groupConfig.groupUsers.push(playerName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      success: true,
 | 
			
		||||
      message: `Add player ${playerName} to ${groupConfig.groupName}`,
 | 
			
		||||
      shouldSaveConfig: true,
 | 
			
		||||
      config,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除用户命令
 | 
			
		||||
class DelCommand implements CLICommand {
 | 
			
		||||
  name = "del";
 | 
			
		||||
  description = "Delete player from group";
 | 
			
		||||
  usage = "del <userGroup> <playerName>";
 | 
			
		||||
 | 
			
		||||
  execute(args: string[], _executor: string, context: CLIContext): CLIResult {
 | 
			
		||||
    if (args.length !== 2) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Usage: ${this.usage}`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [groupName, playerName] = args;
 | 
			
		||||
 | 
			
		||||
    if (groupName === "admin") {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: "Could't delete admin, please edit config",
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const config: AccessConfig = loadConfig(context.configFilepath)!;
 | 
			
		||||
    const groupNames = getGroupNames(config);
 | 
			
		||||
 | 
			
		||||
    if (!groupNames.includes(groupName)) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Invalid group: ${groupName}. Available groups: ${groupNames.join(
 | 
			
		||||
          ", ",
 | 
			
		||||
        )}`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const groupConfig = config.usersGroups.find(
 | 
			
		||||
      (value) => value.groupName === groupName,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!groupConfig) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Group ${groupName} not found`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (groupConfig.groupUsers === undefined) {
 | 
			
		||||
      groupConfig.groupUsers = [];
 | 
			
		||||
    } else {
 | 
			
		||||
      groupConfig.groupUsers = groupConfig.groupUsers.filter(
 | 
			
		||||
        (user) => user !== playerName,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      success: true,
 | 
			
		||||
      message: `Delete ${groupConfig.groupName} ${playerName}`,
 | 
			
		||||
      shouldSaveConfig: true,
 | 
			
		||||
      config,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 列表命令
 | 
			
		||||
class ListCommand implements CLICommand {
 | 
			
		||||
  name = "list";
 | 
			
		||||
  description = "List all players with their groups";
 | 
			
		||||
  usage = "list";
 | 
			
		||||
 | 
			
		||||
  execute(_args: string[], _executor: string, context: CLIContext): CLIResult {
 | 
			
		||||
const addCommand: Command<AppContext> = {
 | 
			
		||||
  name: "add",
 | 
			
		||||
  description: "添加玩家到用户组",
 | 
			
		||||
  args: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "userGroup",
 | 
			
		||||
      description: "要添加到的用户组",
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    { name: "playerName", description: "要添加的玩家", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ args, context }) => {
 | 
			
		||||
    const [groupName, playerName] = [
 | 
			
		||||
      args.userGroup as string,
 | 
			
		||||
      args.playerName as string,
 | 
			
		||||
    ];
 | 
			
		||||
    const config = loadConfig(context.configFilepath)!;
 | 
			
		||||
    let message = `Admins : [ ${config.adminGroupConfig.groupUsers.join(", ")} ]\n`;
 | 
			
		||||
 | 
			
		||||
    if (groupName === "admin") {
 | 
			
		||||
      if (!config.adminGroupConfig.groupUsers.includes(playerName)) {
 | 
			
		||||
        config.adminGroupConfig.groupUsers.push(playerName);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const group = config.usersGroups.find((g) => g.groupName === groupName);
 | 
			
		||||
      if (!group) {
 | 
			
		||||
        const groupNames = getGroupNames(config);
 | 
			
		||||
        context.print({
 | 
			
		||||
          text: `无效的用户组: ${groupName}. 可用用户组: ${groupNames.join(
 | 
			
		||||
            ", ",
 | 
			
		||||
          )}`,
 | 
			
		||||
        });
 | 
			
		||||
        return Ok.EMPTY;
 | 
			
		||||
      }
 | 
			
		||||
      group.groupUsers ??= [];
 | 
			
		||||
      if (!group.groupUsers.includes(playerName)) {
 | 
			
		||||
        group.groupUsers.push(playerName);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    saveConfig(config, context.configFilepath);
 | 
			
		||||
    context.reloadConfig();
 | 
			
		||||
    context.print({ text: `已添加玩家 ${playerName} 到 ${groupName}` });
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const delCommand: Command<AppContext> = {
 | 
			
		||||
  name: "del",
 | 
			
		||||
  description: "从用户组删除玩家",
 | 
			
		||||
  args: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "userGroup",
 | 
			
		||||
      description: "要从中删除玩家的用户组",
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    { name: "playerName", description: "要删除的玩家", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ args, context }) => {
 | 
			
		||||
    const [groupName, playerName] = [
 | 
			
		||||
      args.userGroup as string,
 | 
			
		||||
      args.playerName as string,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    if (groupName === "admin") {
 | 
			
		||||
      context.print({ text: "无法删除管理员, 请直接编辑配置文件。" });
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const config = loadConfig(context.configFilepath)!;
 | 
			
		||||
    const group = config.usersGroups.find((g) => g.groupName === groupName);
 | 
			
		||||
 | 
			
		||||
    if (!group) {
 | 
			
		||||
      const groupNames = getGroupNames(config);
 | 
			
		||||
      context.print({
 | 
			
		||||
        text: `无效的用户组: ${groupName}. 可用用户组: ${groupNames.join(
 | 
			
		||||
          ", ",
 | 
			
		||||
        )}`,
 | 
			
		||||
      });
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (group.groupUsers !== undefined) {
 | 
			
		||||
      group.groupUsers = group.groupUsers.filter((user) => user !== playerName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    saveConfig(config, context.configFilepath);
 | 
			
		||||
    context.reloadConfig();
 | 
			
		||||
    context.print({ text: `已从 ${groupName} 中删除玩家 ${playerName}` });
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const listCommand: Command<AppContext> = {
 | 
			
		||||
  name: "list",
 | 
			
		||||
  description: "列出所有玩家及其所在的用户组",
 | 
			
		||||
  action: ({ context }) => {
 | 
			
		||||
    const config = loadConfig(context.configFilepath)!;
 | 
			
		||||
    let message = `管理员 : [ ${config.adminGroupConfig.groupUsers.join(
 | 
			
		||||
      ", ",
 | 
			
		||||
    )} ]\n`;
 | 
			
		||||
    for (const groupConfig of config.usersGroups) {
 | 
			
		||||
      const users = groupConfig.groupUsers ?? [];
 | 
			
		||||
      message += `${groupConfig.groupName} : [ ${users.join(", ")} ]\n`;
 | 
			
		||||
    }
 | 
			
		||||
    context.print({ text: message.trim() });
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      success: true,
 | 
			
		||||
      message: message.trim(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 设置命令
 | 
			
		||||
class SetCommand implements CLICommand {
 | 
			
		||||
  name = "set";
 | 
			
		||||
  description = "Config access control settings";
 | 
			
		||||
  usage = "set <option> <value>";
 | 
			
		||||
 | 
			
		||||
  execute(args: string[], _executor: string, context: CLIContext): CLIResult {
 | 
			
		||||
    if (args.length !== 2) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Usage: ${this.usage}\nOptions: warnInterval, detectInterval, detectRange`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [option, valueStr] = args;
 | 
			
		||||
const setCommand: Command<AppContext> = {
 | 
			
		||||
  name: "set",
 | 
			
		||||
  description: "配置访问控制设置",
 | 
			
		||||
  args: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "option",
 | 
			
		||||
      description: "要设置的选项 (warnInterval, detectInterval, detectRange)",
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    { name: "value", description: "要设置的值", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ args, context }) => {
 | 
			
		||||
    const [option, valueStr] = [args.option as string, args.value as string];
 | 
			
		||||
    const value = parseInt(valueStr);
 | 
			
		||||
 | 
			
		||||
    if (isNaN(value)) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Invalid value: ${valueStr}. Must be a number.`,
 | 
			
		||||
      };
 | 
			
		||||
      context.print({ text: `无效的值: ${valueStr}. 必须是一个数字。` });
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const config: AccessConfig = loadConfig(context.configFilepath)!;
 | 
			
		||||
    const config = loadConfig(context.configFilepath)!;
 | 
			
		||||
    let message = "";
 | 
			
		||||
 | 
			
		||||
    switch (option) {
 | 
			
		||||
      case "warnInterval":
 | 
			
		||||
        config.watchInterval = value;
 | 
			
		||||
        return {
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: `Set warn interval to ${config.watchInterval}`,
 | 
			
		||||
          shouldSaveConfig: true,
 | 
			
		||||
          config,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        message = `已设置警告间隔为 ${value}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case "detectInterval":
 | 
			
		||||
        config.detectInterval = value;
 | 
			
		||||
        return {
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: `Set detect interval to ${config.detectInterval}`,
 | 
			
		||||
          shouldSaveConfig: true,
 | 
			
		||||
          config,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        message = `已设置检测间隔为 ${value}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case "detectRange":
 | 
			
		||||
        config.detectRange = value;
 | 
			
		||||
        return {
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: `Set detect range to ${config.detectRange}`,
 | 
			
		||||
          shouldSaveConfig: true,
 | 
			
		||||
          config,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        message = `已设置检测范围为 ${value}`;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        return {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: `Unknown option: ${option}. Available options: warnInterval, detectInterval, detectRange`,
 | 
			
		||||
        };
 | 
			
		||||
        context.print({
 | 
			
		||||
          text: `未知选项: ${option}. 可用选项: warnInterval, detectInterval, detectRange`,
 | 
			
		||||
        });
 | 
			
		||||
        return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 帮助命令
 | 
			
		||||
class HelpCommand implements CLICommand {
 | 
			
		||||
  name = "help";
 | 
			
		||||
  description = "Show command help";
 | 
			
		||||
  usage = "help";
 | 
			
		||||
    saveConfig(config, context.configFilepath);
 | 
			
		||||
    context.reloadConfig();
 | 
			
		||||
    context.print({ text: message });
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  execute(_args: string[], _executor: string, context: CLIContext): CLIResult {
 | 
			
		||||
const editGroupCommand: Command<AppContext> = {
 | 
			
		||||
  name: "group",
 | 
			
		||||
  description: "编辑用户组属性",
 | 
			
		||||
  args: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "groupName",
 | 
			
		||||
      description: "要编辑的用户组名称",
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: "property",
 | 
			
		||||
      description: "要更改的属性 (isAllowed, isNotice)",
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    { name: "value", description: "新值 (true/false)", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ args, context }) => {
 | 
			
		||||
    const [groupName, property, valueStr] = [
 | 
			
		||||
      args.groupName as string,
 | 
			
		||||
      args.property as string,
 | 
			
		||||
      args.value as string,
 | 
			
		||||
    ];
 | 
			
		||||
    const config = loadConfig(context.configFilepath)!;
 | 
			
		||||
    const groupNames = getGroupNames(config);
 | 
			
		||||
    const helpMessage = `
 | 
			
		||||
Command Usage: @AC /<Command> [args]
 | 
			
		||||
Commands:
 | 
			
		||||
  - add <userGroup> <playerName>
 | 
			
		||||
      add player to group
 | 
			
		||||
      userGroup: ${groupNames.join(", ")}
 | 
			
		||||
  - del <userGroup> <playerName>
 | 
			
		||||
      delete player in the group, except Admin
 | 
			
		||||
      userGroup: ${groupNames.join(", ")}
 | 
			
		||||
  - list
 | 
			
		||||
      list all of the player with its group
 | 
			
		||||
  - set <options> [params]
 | 
			
		||||
      config access control settings
 | 
			
		||||
      options: warnInterval, detectInterval, detectRange
 | 
			
		||||
  - edit <target> [args]
 | 
			
		||||
      edit various configurations
 | 
			
		||||
      targets: group (edit group properties)
 | 
			
		||||
      examples: edit group <groupName> <property> <value> (properties: isAllowed, isNotice)
 | 
			
		||||
  - showconfig [type]
 | 
			
		||||
      show configuration (type: groups/toast/all)
 | 
			
		||||
  - help
 | 
			
		||||
      show this help message
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      success: true,
 | 
			
		||||
      message: helpMessage.trim(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 统一编辑命令
 | 
			
		||||
class EditCommand implements CLICommand {
 | 
			
		||||
  name = "edit";
 | 
			
		||||
  description = "Edit various configurations (only group now)";
 | 
			
		||||
  usage = "edit <target> [args]";
 | 
			
		||||
 | 
			
		||||
  execute(args: string[], _executor: string, context: CLIContext): CLIResult {
 | 
			
		||||
    if (args.length < 1) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Usage: ${this.usage}\nTargets: group`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [target, ...rest] = args;
 | 
			
		||||
 | 
			
		||||
    switch (target) {
 | 
			
		||||
      case "group":
 | 
			
		||||
        return this.editGroup(rest, context);
 | 
			
		||||
      default:
 | 
			
		||||
        return {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: `Unknown target: ${target}. Available: group`,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private editGroup(args: string[], context: CLIContext): CLIResult {
 | 
			
		||||
    if (args.length !== 3) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Usage: edit group <groupName> <property> <value>\nProperties: isAllowed, isNotice`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [groupName, property, valueStr] = args;
 | 
			
		||||
    const config: AccessConfig = loadConfig(context.configFilepath)!;
 | 
			
		||||
 | 
			
		||||
    let groupConfig: UserGroupConfig | undefined;
 | 
			
		||||
 | 
			
		||||
    if (groupName === "admin") {
 | 
			
		||||
      groupConfig = config.adminGroupConfig;
 | 
			
		||||
    } else {
 | 
			
		||||
      groupConfig = config.usersGroups.find(
 | 
			
		||||
        (group) => group.groupName === groupName,
 | 
			
		||||
      );
 | 
			
		||||
      groupConfig = config.usersGroups.find((g) => g.groupName === groupName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!groupConfig) {
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        message: `Group ${groupName} not found`,
 | 
			
		||||
      };
 | 
			
		||||
      context.print({ text: `用户组 ${groupName} 未找到` });
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const boolValue = parseBoolean(valueStr);
 | 
			
		||||
    if (boolValue === undefined) {
 | 
			
		||||
      context.print({
 | 
			
		||||
        text: `无效的布尔值: ${valueStr}. 请使用 'true' 或 'false'.`,
 | 
			
		||||
      });
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let message = "";
 | 
			
		||||
    switch (property) {
 | 
			
		||||
      case "isAllowed": {
 | 
			
		||||
        const val = parseBoolean(valueStr);
 | 
			
		||||
        if (val != undefined) {
 | 
			
		||||
          groupConfig.isAllowed = val;
 | 
			
		||||
          return {
 | 
			
		||||
            success: true,
 | 
			
		||||
            message: `Set ${groupName}.isAllowed to ${groupConfig.isAllowed}`,
 | 
			
		||||
            shouldSaveConfig: true,
 | 
			
		||||
            config,
 | 
			
		||||
          };
 | 
			
		||||
        } else {
 | 
			
		||||
          return {
 | 
			
		||||
            success: false,
 | 
			
		||||
            message: `Set ${groupName}.isAllowed failed`,
 | 
			
		||||
            shouldSaveConfig: false,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      case "isNotice": {
 | 
			
		||||
        const val = parseBoolean(valueStr);
 | 
			
		||||
        if (val != undefined) {
 | 
			
		||||
          groupConfig.isNotice = val;
 | 
			
		||||
          return {
 | 
			
		||||
            success: true,
 | 
			
		||||
            message: `Set ${groupName}.isNotice to ${groupConfig.isNotice}`,
 | 
			
		||||
            shouldSaveConfig: true,
 | 
			
		||||
            config,
 | 
			
		||||
          };
 | 
			
		||||
        } else {
 | 
			
		||||
          return {
 | 
			
		||||
            success: false,
 | 
			
		||||
            message: `Set ${groupName}.isAllowed failed`,
 | 
			
		||||
            shouldSaveConfig: false,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      case "isAllowed":
 | 
			
		||||
        groupConfig.isAllowed = boolValue;
 | 
			
		||||
        message = `已设置 ${groupName}.isAllowed 为 ${boolValue}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case "isNotice":
 | 
			
		||||
        groupConfig.isNotice = boolValue;
 | 
			
		||||
        message = `已设置 ${groupName}.isNotice 为 ${boolValue}`;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        return {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: `Unknown property: ${property}. Available: isAllowed, isNotice`,
 | 
			
		||||
        };
 | 
			
		||||
        context.print({
 | 
			
		||||
          text: `未知属性: ${property}. 可用属性: isAllowed, isNotice`,
 | 
			
		||||
        });
 | 
			
		||||
        return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 显示配置命令
 | 
			
		||||
class ShowConfigCommand implements CLICommand {
 | 
			
		||||
  name = "showconfig";
 | 
			
		||||
  description = "Show configuration";
 | 
			
		||||
  usage = "showconfig [type]";
 | 
			
		||||
    saveConfig(config, context.configFilepath);
 | 
			
		||||
    context.reloadConfig();
 | 
			
		||||
    context.print({ text: message });
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  execute(args: string[], _executor: string, context: CLIContext): CLIResult {
 | 
			
		||||
    const type = args[0] || "all";
 | 
			
		||||
const editCommand: Command<AppContext> = {
 | 
			
		||||
  name: "edit",
 | 
			
		||||
  description: "编辑各项配置",
 | 
			
		||||
  subcommands: new Map([["group", editGroupCommand]]),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showConfigCommand: Command<AppContext> = {
 | 
			
		||||
  name: "showconfig",
 | 
			
		||||
  description: "显示配置",
 | 
			
		||||
  options: new Map([
 | 
			
		||||
    [
 | 
			
		||||
      "type",
 | 
			
		||||
      {
 | 
			
		||||
        name: "type",
 | 
			
		||||
        description: "要显示的配置类型 (groups, toast, all)",
 | 
			
		||||
        required: false,
 | 
			
		||||
        defaultValue: "all",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ options, context }) => {
 | 
			
		||||
    const type = options.type as string;
 | 
			
		||||
    const config = loadConfig(context.configFilepath)!;
 | 
			
		||||
    let message = "";
 | 
			
		||||
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case "groups": {
 | 
			
		||||
        let groupsMessage = `Admin Group: ${config.adminGroupConfig.groupName}\n`;
 | 
			
		||||
        groupsMessage += `  Users: [${config.adminGroupConfig.groupUsers.join(", ")}]\n`;
 | 
			
		||||
        groupsMessage += `  Allowed: ${config.adminGroupConfig.isAllowed}\n`;
 | 
			
		||||
        groupsMessage += `  notice: ${config.adminGroupConfig.isNotice}\n\n`;
 | 
			
		||||
        let groupsMessage = `管理员组: ${config.adminGroupConfig.groupName}\n`;
 | 
			
		||||
        groupsMessage += `  用户: [${config.adminGroupConfig.groupUsers.join(
 | 
			
		||||
          ", ",
 | 
			
		||||
        )}]\n`;
 | 
			
		||||
        groupsMessage += `  允许: ${config.adminGroupConfig.isAllowed}\n`;
 | 
			
		||||
        groupsMessage += `  通知: ${config.adminGroupConfig.isNotice}\n\n`;
 | 
			
		||||
 | 
			
		||||
        for (const group of config.usersGroups) {
 | 
			
		||||
          groupsMessage += `Group: ${group.groupName}\n`;
 | 
			
		||||
          groupsMessage += `  Users: [${(group.groupUsers ?? []).join(", ")}]\n`;
 | 
			
		||||
          groupsMessage += `  Allowed: ${group.isAllowed}\n`;
 | 
			
		||||
          groupsMessage += `  Notice: ${group.isNotice}\n`;
 | 
			
		||||
          groupsMessage += `用户组: ${group.groupName}\n`;
 | 
			
		||||
          groupsMessage += `  用户: [${(group.groupUsers ?? []).join(", ")}]\n`;
 | 
			
		||||
          groupsMessage += `  允许: ${group.isAllowed}\n`;
 | 
			
		||||
          groupsMessage += `  通知: ${group.isNotice}\n`;
 | 
			
		||||
          groupsMessage += "\n";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: groupsMessage.trim(),
 | 
			
		||||
        };
 | 
			
		||||
        message = groupsMessage.trim();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      case "toast": {
 | 
			
		||||
        let toastMessage = "Default Toast Config:\n";
 | 
			
		||||
        toastMessage += `  Title: ${config.welcomeToastConfig.title.text}\n`;
 | 
			
		||||
        toastMessage += `  Message: ${config.welcomeToastConfig.msg.text}\n`;
 | 
			
		||||
        toastMessage += `  Prefix: ${config.welcomeToastConfig.prefix ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Brackets: ${config.welcomeToastConfig.brackets ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Bracket Color: ${config.welcomeToastConfig.bracketColor ?? "none"}\n\n`;
 | 
			
		||||
        let toastMessage = "默认 Toast 配置:\n";
 | 
			
		||||
        toastMessage += `  标题: ${config.welcomeToastConfig.title.text}\n`;
 | 
			
		||||
        toastMessage += `  消息: ${config.welcomeToastConfig.msg.text}\n`;
 | 
			
		||||
        toastMessage += `  前缀: ${
 | 
			
		||||
          config.welcomeToastConfig.prefix ?? "none"
 | 
			
		||||
        }\n`;
 | 
			
		||||
        toastMessage += `  括号: ${
 | 
			
		||||
          config.welcomeToastConfig.brackets ?? "none"
 | 
			
		||||
        }\n`;
 | 
			
		||||
        toastMessage += `  括号颜色: ${
 | 
			
		||||
          config.welcomeToastConfig.bracketColor ?? "none"
 | 
			
		||||
        }\n\n`;
 | 
			
		||||
 | 
			
		||||
        toastMessage += "Warn Toast Config:\n";
 | 
			
		||||
        toastMessage += `  Title: ${config.warnToastConfig.title.text}\n`;
 | 
			
		||||
        toastMessage += `  Message: ${config.warnToastConfig.msg.text}\n`;
 | 
			
		||||
        toastMessage += `  Prefix: ${config.warnToastConfig.prefix ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Brackets: ${config.warnToastConfig.brackets ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Bracket Color: ${config.warnToastConfig.bracketColor ?? "none"}`;
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: toastMessage,
 | 
			
		||||
        };
 | 
			
		||||
        toastMessage += "警告 Toast 配置:\n";
 | 
			
		||||
        toastMessage += `  标题: ${config.warnToastConfig.title.text}\n`;
 | 
			
		||||
        toastMessage += `  消息: ${config.warnToastConfig.msg.text}\n`;
 | 
			
		||||
        toastMessage += `  前缀: ${config.warnToastConfig.prefix ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  括号: ${
 | 
			
		||||
          config.warnToastConfig.brackets ?? "none"
 | 
			
		||||
        }\n`;
 | 
			
		||||
        toastMessage += `  括号颜色: ${
 | 
			
		||||
          config.warnToastConfig.bracketColor ?? "none"
 | 
			
		||||
        }`;
 | 
			
		||||
        message = toastMessage;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      case "all": {
 | 
			
		||||
        let allMessage = `Detect Range: ${config.detectRange}\n`;
 | 
			
		||||
        allMessage += `Detect Interval: ${config.detectInterval}\n`;
 | 
			
		||||
        allMessage += `Warn Interval: ${config.watchInterval}\n\n`;
 | 
			
		||||
        let allMessage = `检测范围: ${config.detectRange}\n`;
 | 
			
		||||
        allMessage += `检测间隔: ${config.detectInterval}\n`;
 | 
			
		||||
        allMessage += `警告间隔: ${config.watchInterval}\n\n`;
 | 
			
		||||
        allMessage +=
 | 
			
		||||
          "Use 'showconfig groups' or 'showconfig toast' for detailed view";
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: allMessage,
 | 
			
		||||
        };
 | 
			
		||||
          "使用 'showconfig --type groups' 或 'showconfig --type toast' 查看详细信息";
 | 
			
		||||
        message = allMessage;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        return {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: `Invalid type: ${type}. Available: groups, toast, all`,
 | 
			
		||||
        };
 | 
			
		||||
        message = `无效类型: ${type}. 可用类型: groups, toast, all`;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CLI循环处理器
 | 
			
		||||
export class AccessControlCLI {
 | 
			
		||||
  private processor: CLICommandProcessor;
 | 
			
		||||
  private context: CLIContext;
 | 
			
		||||
 | 
			
		||||
  constructor(context: CLIContext) {
 | 
			
		||||
    this.context = context;
 | 
			
		||||
    this.processor = new CLICommandProcessor(context);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public startConfigLoop() {
 | 
			
		||||
    while (true) {
 | 
			
		||||
      const ev = pullEventAs(ChatBoxEvent, "chat");
 | 
			
		||||
 | 
			
		||||
      if (ev === undefined) continue;
 | 
			
		||||
 | 
			
		||||
      const config = loadConfig(this.context.configFilepath)!;
 | 
			
		||||
      if (!config.adminGroupConfig.groupUsers.includes(ev.username)) continue;
 | 
			
		||||
      if (!ev.message.startsWith("@AC")) continue;
 | 
			
		||||
 | 
			
		||||
      this.context.log.info(
 | 
			
		||||
        `Received command "${ev.message}" from admin ${ev.username}`,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const result = this.processor.processCommand(ev.message, ev.username);
 | 
			
		||||
      this.processor.sendResponse(result, ev.username);
 | 
			
		||||
      if (!result.success) {
 | 
			
		||||
        this.context.log.warn(`Command failed: ${result.message}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 导出类型和工厂函数
 | 
			
		||||
export { CLIContext, CLIResult, CLICommand };
 | 
			
		||||
 | 
			
		||||
export function createAccessControlCLI(context: CLIContext): AccessControlCLI {
 | 
			
		||||
  return new AccessControlCLI(context);
 | 
			
		||||
    context.print({ text: message });
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Root command
 | 
			
		||||
const rootCommand: Command<AppContext> = {
 | 
			
		||||
  name: "@AC",
 | 
			
		||||
  description: "访问控制命令行界面",
 | 
			
		||||
  subcommands: new Map([
 | 
			
		||||
    ["add", addCommand],
 | 
			
		||||
    ["del", delCommand],
 | 
			
		||||
    ["list", listCommand],
 | 
			
		||||
    ["set", setCommand],
 | 
			
		||||
    ["edit", editCommand],
 | 
			
		||||
    ["showconfig", showConfigCommand],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ context }) => {
 | 
			
		||||
    context.print([
 | 
			
		||||
      {
 | 
			
		||||
        text: "请使用 ",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: "@AC --help",
 | 
			
		||||
        clickEvent: {
 | 
			
		||||
          action: "copy_to_clipboard",
 | 
			
		||||
          value: "@AC --help",
 | 
			
		||||
        },
 | 
			
		||||
        hoverEvent: {
 | 
			
		||||
          action: "show_text",
 | 
			
		||||
          value: "点击复制命令",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: " 获取门禁系统更详细的命令说明😊😊😊",
 | 
			
		||||
      },
 | 
			
		||||
    ]);
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function createAccessControlCli(context: AppContext) {
 | 
			
		||||
  return createCli(rootCommand, {
 | 
			
		||||
    globalContext: context,
 | 
			
		||||
    writer: (msg) => context.print(msg),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ interface UserGroupConfig {
 | 
			
		||||
  groupName: string;
 | 
			
		||||
  isAllowed: boolean;
 | 
			
		||||
  isNotice: boolean;
 | 
			
		||||
  isWelcome: boolean;
 | 
			
		||||
  groupUsers: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -39,6 +40,7 @@ const defaultConfig: AccessConfig = {
 | 
			
		||||
    groupUsers: ["Selcon"],
 | 
			
		||||
    isAllowed: true,
 | 
			
		||||
    isNotice: true,
 | 
			
		||||
    isWelcome: true,
 | 
			
		||||
  },
 | 
			
		||||
  usersGroups: [
 | 
			
		||||
    {
 | 
			
		||||
@@ -46,57 +48,60 @@ const defaultConfig: AccessConfig = {
 | 
			
		||||
      groupUsers: [],
 | 
			
		||||
      isAllowed: true,
 | 
			
		||||
      isNotice: true,
 | 
			
		||||
      isWelcome: false,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      groupName: "VIP",
 | 
			
		||||
      groupUsers: [],
 | 
			
		||||
      isAllowed: true,
 | 
			
		||||
      isNotice: false,
 | 
			
		||||
      isWelcome: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      groupName: "enemies",
 | 
			
		||||
      groupUsers: [],
 | 
			
		||||
      isAllowed: false,
 | 
			
		||||
      isNotice: false,
 | 
			
		||||
      isWelcome: false,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  welcomeToastConfig: {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: "Welcome",
 | 
			
		||||
      text: "欢迎",
 | 
			
		||||
      color: "green",
 | 
			
		||||
    },
 | 
			
		||||
    msg: {
 | 
			
		||||
      text: "Hello User %playerName%",
 | 
			
		||||
      color: "green",
 | 
			
		||||
      text: "欢迎 %playerName% 参观桃源星喵~",
 | 
			
		||||
      color: "#EDC8DA",
 | 
			
		||||
    },
 | 
			
		||||
    prefix: "Taohuayuan",
 | 
			
		||||
    brackets: "[]",
 | 
			
		||||
    prefix: "桃源星",
 | 
			
		||||
    brackets: "<>",
 | 
			
		||||
    bracketColor: "",
 | 
			
		||||
  },
 | 
			
		||||
  noticeToastConfig: {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: "Notice",
 | 
			
		||||
      text: "警告",
 | 
			
		||||
      color: "red",
 | 
			
		||||
    },
 | 
			
		||||
    msg: {
 | 
			
		||||
      text: "Unfamiliar player %playerName% appeared at Position %playerPosX%, %playerPosY%, %playerPosZ%",
 | 
			
		||||
      text: "陌生玩家 %playerName% 出现在 %playerPosX%, %playerPosY%, %playerPosZ%",
 | 
			
		||||
      color: "red",
 | 
			
		||||
    },
 | 
			
		||||
    prefix: "Taohuayuan",
 | 
			
		||||
    brackets: "[]",
 | 
			
		||||
    prefix: "桃源星",
 | 
			
		||||
    brackets: "<>",
 | 
			
		||||
    bracketColor: "",
 | 
			
		||||
  },
 | 
			
		||||
  warnToastConfig: {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: "Attention!!!",
 | 
			
		||||
      text: "注意",
 | 
			
		||||
      color: "red",
 | 
			
		||||
    },
 | 
			
		||||
    msg: {
 | 
			
		||||
      text: "%playerName% you are not allowed to be here",
 | 
			
		||||
      text: "%playerName% 你已经进入桃源星领地",
 | 
			
		||||
      color: "red",
 | 
			
		||||
    },
 | 
			
		||||
    prefix: "Taohuayuan",
 | 
			
		||||
    brackets: "[]",
 | 
			
		||||
    prefix: "桃源星",
 | 
			
		||||
    brackets: "<>",
 | 
			
		||||
    bracketColor: "",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,13 @@
 | 
			
		||||
import { CCLog, DAY, LogLevel } from "@/lib/ccLog";
 | 
			
		||||
import { ToastConfig, UserGroupConfig, loadConfig } from "./config";
 | 
			
		||||
import { createAccessControlCLI } from "./cli";
 | 
			
		||||
import { createAccessControlCli } from "./cli";
 | 
			
		||||
import { launchAccessControlTUI } from "./tui";
 | 
			
		||||
import * as peripheralManager from "../lib/PeripheralManager";
 | 
			
		||||
import { deepCopy } from "@/lib/common";
 | 
			
		||||
import { ReadWriteLock } from "@/lib/mutex/ReadWriteLock";
 | 
			
		||||
import { ChatManager } from "@/lib/ChatManager";
 | 
			
		||||
import { gTimerManager } from "@/lib/TimerManager";
 | 
			
		||||
import { KeyEvent, pullEventAs } from "@/lib/event";
 | 
			
		||||
 | 
			
		||||
const args = [...$vararg];
 | 
			
		||||
 | 
			
		||||
@@ -12,7 +15,7 @@ const args = [...$vararg];
 | 
			
		||||
const logger = new CCLog("accesscontrol.log", {
 | 
			
		||||
  printTerminal: true,
 | 
			
		||||
  logInterval: DAY,
 | 
			
		||||
  outputMinLevel: LogLevel.Info,
 | 
			
		||||
  outputMinLevel: LogLevel.Debug,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Load Config
 | 
			
		||||
@@ -23,16 +26,20 @@ logger.info("Load config successfully!");
 | 
			
		||||
logger.debug(textutils.serialise(config, { allow_repetitions: true }));
 | 
			
		||||
 | 
			
		||||
// Peripheral
 | 
			
		||||
const playerDetector = peripheralManager.findByNameRequired("playerDetector");
 | 
			
		||||
const chatBox = peripheralManager.findByNameRequired("chatBox");
 | 
			
		||||
const playerDetector = peripheral.find(
 | 
			
		||||
  "playerDetector",
 | 
			
		||||
)[0] as PlayerDetectorPeripheral;
 | 
			
		||||
const chatBox = peripheral.find("chatBox")[0] as ChatBoxPeripheral;
 | 
			
		||||
const chatManager: ChatManager = new ChatManager([chatBox]);
 | 
			
		||||
 | 
			
		||||
// Global
 | 
			
		||||
let inRangePlayers: string[] = [];
 | 
			
		||||
let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
 | 
			
		||||
let gInRangePlayers: string[] = [];
 | 
			
		||||
let gWatchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
 | 
			
		||||
let gIsRunning = true;
 | 
			
		||||
 | 
			
		||||
interface ParseParams {
 | 
			
		||||
  name?: string;
 | 
			
		||||
  group?: string;
 | 
			
		||||
  playerName?: string;
 | 
			
		||||
  groupName?: string;
 | 
			
		||||
  info?: PlayerInfo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -44,8 +51,8 @@ function reloadConfig() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  config = loadConfig(configFilepath)!;
 | 
			
		||||
  inRangePlayers = [];
 | 
			
		||||
  watchPlayersInfo = [];
 | 
			
		||||
  gInRangePlayers = [];
 | 
			
		||||
  gWatchPlayersInfo = [];
 | 
			
		||||
  releaser.release();
 | 
			
		||||
  logger.info("Reload config successfully!");
 | 
			
		||||
}
 | 
			
		||||
@@ -53,7 +60,7 @@ function reloadConfig() {
 | 
			
		||||
function safeParseTextComponent(
 | 
			
		||||
  component: MinecraftTextComponent,
 | 
			
		||||
  params?: ParseParams,
 | 
			
		||||
): string {
 | 
			
		||||
): MinecraftTextComponent {
 | 
			
		||||
  const newComponent = deepCopy(component);
 | 
			
		||||
 | 
			
		||||
  if (newComponent.text == undefined) {
 | 
			
		||||
@@ -61,11 +68,11 @@ function safeParseTextComponent(
 | 
			
		||||
  } else if (newComponent.text.includes("%")) {
 | 
			
		||||
    newComponent.text = newComponent.text.replace(
 | 
			
		||||
      "%playerName%",
 | 
			
		||||
      params?.name ?? "UnknowPlayer",
 | 
			
		||||
      params?.playerName ?? "UnknowPlayer",
 | 
			
		||||
    );
 | 
			
		||||
    newComponent.text = newComponent.text.replace(
 | 
			
		||||
      "%groupName%",
 | 
			
		||||
      params?.group ?? "UnknowGroup",
 | 
			
		||||
      params?.groupName ?? "UnknowGroup",
 | 
			
		||||
    );
 | 
			
		||||
    newComponent.text = newComponent.text.replace(
 | 
			
		||||
      "%playerPosX%",
 | 
			
		||||
@@ -80,7 +87,34 @@ function safeParseTextComponent(
 | 
			
		||||
      params?.info?.z.toString() ?? "UnknowPosZ",
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return textutils.serialiseJSON(newComponent);
 | 
			
		||||
  return newComponent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendMessage(
 | 
			
		||||
  toastConfig: ToastConfig,
 | 
			
		||||
  targetPlayer: string,
 | 
			
		||||
  params: ParseParams,
 | 
			
		||||
) {
 | 
			
		||||
  let releaser = configLock.tryAcquireRead();
 | 
			
		||||
  while (releaser === undefined) {
 | 
			
		||||
    sleep(0.1);
 | 
			
		||||
    releaser = configLock.tryAcquireRead();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  chatManager.sendMessage({
 | 
			
		||||
    message: safeParseTextComponent(
 | 
			
		||||
      toastConfig.msg ?? config.welcomeToastConfig.msg,
 | 
			
		||||
      params,
 | 
			
		||||
    ),
 | 
			
		||||
    prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
 | 
			
		||||
    brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
 | 
			
		||||
    bracketColor:
 | 
			
		||||
      toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
 | 
			
		||||
    targetPlayer: targetPlayer,
 | 
			
		||||
    utf8Support: true,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  releaser.release();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendToast(
 | 
			
		||||
@@ -94,22 +128,22 @@ function sendToast(
 | 
			
		||||
    releaser = configLock.tryAcquireRead();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  chatBox.sendFormattedToastToPlayer(
 | 
			
		||||
    safeParseTextComponent(
 | 
			
		||||
  chatManager.sendToast({
 | 
			
		||||
    message: safeParseTextComponent(
 | 
			
		||||
      toastConfig.msg ?? config.welcomeToastConfig.msg,
 | 
			
		||||
      params,
 | 
			
		||||
    ),
 | 
			
		||||
    safeParseTextComponent(
 | 
			
		||||
    title: safeParseTextComponent(
 | 
			
		||||
      toastConfig.title ?? config.welcomeToastConfig.title,
 | 
			
		||||
      params,
 | 
			
		||||
    ),
 | 
			
		||||
    targetPlayer,
 | 
			
		||||
    toastConfig.prefix ?? config.welcomeToastConfig.prefix,
 | 
			
		||||
    toastConfig.brackets ?? config.welcomeToastConfig.brackets,
 | 
			
		||||
    toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
 | 
			
		||||
    undefined,
 | 
			
		||||
    true,
 | 
			
		||||
  );
 | 
			
		||||
    prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
 | 
			
		||||
    brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
 | 
			
		||||
    bracketColor:
 | 
			
		||||
      toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
 | 
			
		||||
    targetPlayer: targetPlayer,
 | 
			
		||||
    utf8Support: true,
 | 
			
		||||
  });
 | 
			
		||||
  releaser.release();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -131,7 +165,7 @@ function sendNotice(player: string, playerInfo?: PlayerInfo) {
 | 
			
		||||
  for (const targetPlayer of noticeTargetPlayers) {
 | 
			
		||||
    if (!onlinePlayers.includes(targetPlayer)) continue;
 | 
			
		||||
    sendToast(config.noticeToastConfig, targetPlayer, {
 | 
			
		||||
      name: player,
 | 
			
		||||
      playerName: player,
 | 
			
		||||
      info: playerInfo,
 | 
			
		||||
    });
 | 
			
		||||
    sleep(1);
 | 
			
		||||
@@ -149,32 +183,32 @@ function sendWarn(player: string) {
 | 
			
		||||
    releaser = configLock.tryAcquireRead();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sendToast(config.warnToastConfig, player, { name: player });
 | 
			
		||||
  chatBox.sendFormattedMessageToPlayer(
 | 
			
		||||
    safeParseTextComponent(config.warnToastConfig.msg, { name: player }),
 | 
			
		||||
    player,
 | 
			
		||||
    "AccessControl",
 | 
			
		||||
    "[]",
 | 
			
		||||
    undefined,
 | 
			
		||||
    undefined,
 | 
			
		||||
    true,
 | 
			
		||||
  );
 | 
			
		||||
  sendToast(config.warnToastConfig, player, { playerName: player });
 | 
			
		||||
  chatManager.sendMessage({
 | 
			
		||||
    message: safeParseTextComponent(config.warnToastConfig.msg, {
 | 
			
		||||
      playerName: player,
 | 
			
		||||
    }),
 | 
			
		||||
    targetPlayer: player,
 | 
			
		||||
    prefix: "AccessControl",
 | 
			
		||||
    brackets: "[]",
 | 
			
		||||
    utf8Support: true,
 | 
			
		||||
  });
 | 
			
		||||
  releaser.release();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function watchLoop() {
 | 
			
		||||
  while (true) {
 | 
			
		||||
  while (gIsRunning) {
 | 
			
		||||
    const releaser = configLock.tryAcquireRead();
 | 
			
		||||
    if (releaser === undefined) {
 | 
			
		||||
      os.sleep(1);
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const watchPlayerNames = watchPlayersInfo.flatMap((value) => value.name);
 | 
			
		||||
    const watchPlayerNames = gWatchPlayersInfo.flatMap((value) => value.name);
 | 
			
		||||
    logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
 | 
			
		||||
    for (const player of watchPlayersInfo) {
 | 
			
		||||
    for (const player of gWatchPlayersInfo) {
 | 
			
		||||
      const playerInfo = playerDetector.getPlayerPos(player.name);
 | 
			
		||||
      if (inRangePlayers.includes(player.name)) {
 | 
			
		||||
      if (gInRangePlayers.includes(player.name)) {
 | 
			
		||||
        // Notice
 | 
			
		||||
        if (player.hasNoticeTimes < config.noticeTimes) {
 | 
			
		||||
          sendNotice(player.name, playerInfo);
 | 
			
		||||
@@ -190,7 +224,7 @@ function watchLoop() {
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        // Get rid of player from list
 | 
			
		||||
        watchPlayersInfo = watchPlayersInfo.filter(
 | 
			
		||||
        gWatchPlayersInfo = gWatchPlayersInfo.filter(
 | 
			
		||||
          (value) => value.name != player.name,
 | 
			
		||||
        );
 | 
			
		||||
        logger.info(
 | 
			
		||||
@@ -206,7 +240,7 @@ function watchLoop() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mainLoop() {
 | 
			
		||||
  while (true) {
 | 
			
		||||
  while (gIsRunning) {
 | 
			
		||||
    const releaser = configLock.tryAcquireRead();
 | 
			
		||||
    if (releaser === undefined) {
 | 
			
		||||
      os.sleep(0.1);
 | 
			
		||||
@@ -218,20 +252,31 @@ function mainLoop() {
 | 
			
		||||
    logger.debug(`Detected ${players.length} players: ${playersList}`);
 | 
			
		||||
 | 
			
		||||
    for (const player of players) {
 | 
			
		||||
      if (inRangePlayers.includes(player)) continue;
 | 
			
		||||
      if (gInRangePlayers.includes(player)) continue;
 | 
			
		||||
 | 
			
		||||
      // Get player Info
 | 
			
		||||
      const playerInfo = playerDetector.getPlayerPos(player);
 | 
			
		||||
 | 
			
		||||
      if (config.adminGroupConfig.groupUsers.includes(player)) {
 | 
			
		||||
        logger.info(`Admin ${player} appear`);
 | 
			
		||||
        logger.info(
 | 
			
		||||
          `Admin ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
 | 
			
		||||
        );
 | 
			
		||||
        if (config.adminGroupConfig.isWelcome)
 | 
			
		||||
          sendMessage(config.welcomeToastConfig, player, {
 | 
			
		||||
            playerName: player,
 | 
			
		||||
            groupName: "Admin",
 | 
			
		||||
            info: playerInfo,
 | 
			
		||||
          });
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // New player appear
 | 
			
		||||
      const playerInfo = playerDetector.getPlayerPos(player);
 | 
			
		||||
      let groupConfig: UserGroupConfig = {
 | 
			
		||||
        groupName: "Unfamiliar",
 | 
			
		||||
        groupUsers: [],
 | 
			
		||||
        isAllowed: false,
 | 
			
		||||
        isNotice: false,
 | 
			
		||||
        isWelcome: false,
 | 
			
		||||
      };
 | 
			
		||||
      for (const userGroupConfig of config.usersGroups) {
 | 
			
		||||
        if (userGroupConfig.groupUsers == undefined) continue;
 | 
			
		||||
@@ -244,28 +289,37 @@ function mainLoop() {
 | 
			
		||||
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (config.adminGroupConfig.isWelcome)
 | 
			
		||||
        sendMessage(config.welcomeToastConfig, player, {
 | 
			
		||||
          playerName: player,
 | 
			
		||||
          groupName: groupConfig.groupName,
 | 
			
		||||
          info: playerInfo,
 | 
			
		||||
        });
 | 
			
		||||
      if (groupConfig.isAllowed) continue;
 | 
			
		||||
 | 
			
		||||
      logger.warn(
 | 
			
		||||
        `${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (config.isWarn) sendWarn(player);
 | 
			
		||||
      watchPlayersInfo = [
 | 
			
		||||
        ...watchPlayersInfo,
 | 
			
		||||
      gWatchPlayersInfo = [
 | 
			
		||||
        ...gWatchPlayersInfo,
 | 
			
		||||
        { name: player, hasNoticeTimes: 0 },
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inRangePlayers = players;
 | 
			
		||||
    gInRangePlayers = players;
 | 
			
		||||
    releaser.release();
 | 
			
		||||
    os.sleep(config.detectInterval);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function keyboardLoop() {
 | 
			
		||||
  while (true) {
 | 
			
		||||
    const [eventType, key] = os.pullEvent("key");
 | 
			
		||||
    if (eventType === "key" && key === keys.c) {
 | 
			
		||||
  while (gIsRunning) {
 | 
			
		||||
    const event = pullEventAs(KeyEvent, "key");
 | 
			
		||||
    if (event === undefined) continue;
 | 
			
		||||
 | 
			
		||||
    if (event.key === keys.c) {
 | 
			
		||||
      logger.info("Launching Access Control TUI...");
 | 
			
		||||
      try {
 | 
			
		||||
        logger.setInTerminal(false);
 | 
			
		||||
@@ -277,7 +331,66 @@ function keyboardLoop() {
 | 
			
		||||
        logger.setInTerminal(true);
 | 
			
		||||
        reloadConfig();
 | 
			
		||||
      }
 | 
			
		||||
    } else if (event.key === keys.r) {
 | 
			
		||||
      reloadConfig();
 | 
			
		||||
    }
 | 
			
		||||
    // else if (event.key === keys.q) {
 | 
			
		||||
    //   gIsRunning = false;
 | 
			
		||||
    // }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cliLoop() {
 | 
			
		||||
  let printTargetPlayer: string | undefined;
 | 
			
		||||
  const cli = createAccessControlCli({
 | 
			
		||||
    configFilepath: configFilepath,
 | 
			
		||||
    reloadConfig: () => reloadConfig(),
 | 
			
		||||
    logger: logger,
 | 
			
		||||
    print: (msg) =>
 | 
			
		||||
      chatManager.sendMessage({
 | 
			
		||||
        message: msg,
 | 
			
		||||
        targetPlayer: printTargetPlayer,
 | 
			
		||||
        prefix: "Access Control System",
 | 
			
		||||
        brackets: "[]",
 | 
			
		||||
        utf8Support: true,
 | 
			
		||||
      }),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  while (gIsRunning) {
 | 
			
		||||
    const result = chatManager.getReceivedMessage();
 | 
			
		||||
    if (result.isErr()) {
 | 
			
		||||
      sleep(0.5);
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    logger.debug(`Received message: ${result.value.message}`);
 | 
			
		||||
 | 
			
		||||
    const ev = result.value;
 | 
			
		||||
 | 
			
		||||
    let releaser = configLock.tryAcquireRead();
 | 
			
		||||
    while (releaser === undefined) {
 | 
			
		||||
      sleep(0.1);
 | 
			
		||||
      releaser = configLock.tryAcquireRead();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isAdmin = config.adminGroupConfig.groupUsers.includes(ev.username);
 | 
			
		||||
 | 
			
		||||
    releaser.release();
 | 
			
		||||
    if (!isAdmin) continue;
 | 
			
		||||
    if (!ev.message.startsWith("@AC")) continue;
 | 
			
		||||
 | 
			
		||||
    printTargetPlayer = ev.username;
 | 
			
		||||
    logger.info(
 | 
			
		||||
      `Received command "${ev.message}" from admin ${printTargetPlayer}`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const commandArgs = ev.message
 | 
			
		||||
      .substring(3)
 | 
			
		||||
      .split(" ")
 | 
			
		||||
      .filter((s) => s.length > 0);
 | 
			
		||||
    logger.debug(`Command arguments: ${commandArgs.join(", ")}`);
 | 
			
		||||
 | 
			
		||||
    cli(commandArgs);
 | 
			
		||||
    printTargetPlayer = undefined;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -285,30 +398,18 @@ function main(args: string[]) {
 | 
			
		||||
  logger.info("Starting access control system, get args: " + args.join(", "));
 | 
			
		||||
  if (args.length == 1) {
 | 
			
		||||
    if (args[0] == "start") {
 | 
			
		||||
      // 创建CLI处理器
 | 
			
		||||
      const cli = createAccessControlCLI({
 | 
			
		||||
        configFilepath: configFilepath,
 | 
			
		||||
        reloadConfig: () => reloadConfig(),
 | 
			
		||||
        log: logger,
 | 
			
		||||
        chatBox: chatBox,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      print(
 | 
			
		||||
        "Access Control System started. Press 'c' to open configuration TUI.",
 | 
			
		||||
      );
 | 
			
		||||
      const tutorial: string[] = [];
 | 
			
		||||
      tutorial.push("Access Control System started.");
 | 
			
		||||
      tutorial.push("\tPress 'c' to open configuration TUI.");
 | 
			
		||||
      tutorial.push("\tPress 'r' to reload configuration.");
 | 
			
		||||
      print(tutorial.join("\n"));
 | 
			
		||||
      parallel.waitForAll(
 | 
			
		||||
        () => {
 | 
			
		||||
          mainLoop();
 | 
			
		||||
        },
 | 
			
		||||
        () => {
 | 
			
		||||
          cli.startConfigLoop();
 | 
			
		||||
        },
 | 
			
		||||
        () => {
 | 
			
		||||
          watchLoop();
 | 
			
		||||
        },
 | 
			
		||||
        () => {
 | 
			
		||||
          keyboardLoop();
 | 
			
		||||
        },
 | 
			
		||||
        () => mainLoop(),
 | 
			
		||||
        () => gTimerManager.run(),
 | 
			
		||||
        () => cliLoop(),
 | 
			
		||||
        () => watchLoop(),
 | 
			
		||||
        () => keyboardLoop(),
 | 
			
		||||
        () => chatManager.run(),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return;
 | 
			
		||||
 
 | 
			
		||||
@@ -355,6 +355,34 @@ const AccessControlTUI = () => {
 | 
			
		||||
        { class: "flex flex-col ml-2" },
 | 
			
		||||
        label({}, () => `Group: ${getSelectedGroup().groupName}`),
 | 
			
		||||
 | 
			
		||||
        div(
 | 
			
		||||
          { class: "flex flex-row" },
 | 
			
		||||
          label({}, "Is Welcome:"),
 | 
			
		||||
          input({
 | 
			
		||||
            type: "checkbox",
 | 
			
		||||
            checked: () => getSelectedGroup().isWelcome,
 | 
			
		||||
            onChange: (checked) => {
 | 
			
		||||
              const groupIndex = selectedGroupIndex();
 | 
			
		||||
              if (groupIndex === 0) {
 | 
			
		||||
                const currentAdmin = config().adminGroupConfig;
 | 
			
		||||
                setConfig("adminGroupConfig", {
 | 
			
		||||
                  ...currentAdmin,
 | 
			
		||||
                  isWelcome: checked,
 | 
			
		||||
                });
 | 
			
		||||
              } else {
 | 
			
		||||
                const actualIndex = groupIndex - 1;
 | 
			
		||||
                const currentGroups = config().usersGroups;
 | 
			
		||||
                const currentGroup = currentGroups[actualIndex];
 | 
			
		||||
                const newGroups = [...currentGroups];
 | 
			
		||||
                newGroups[actualIndex] = {
 | 
			
		||||
                  ...currentGroup,
 | 
			
		||||
                  isWelcome: checked,
 | 
			
		||||
                };
 | 
			
		||||
                setConfig("usersGroups", newGroups);
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
        div(
 | 
			
		||||
          { class: "flex flex-row" },
 | 
			
		||||
          label({}, "Is Allowed:"),
 | 
			
		||||
@@ -541,7 +569,12 @@ const AccessControlTUI = () => {
 | 
			
		||||
          label({}, "Prefix:"),
 | 
			
		||||
          input({
 | 
			
		||||
            type: "text",
 | 
			
		||||
            value: () => getTempToastConfig().prefix,
 | 
			
		||||
            value: () => {
 | 
			
		||||
              const str = textutils.serialiseJSON(getTempToastConfig().prefix, {
 | 
			
		||||
                unicode_strings: true,
 | 
			
		||||
              });
 | 
			
		||||
              return str.substring(1, str.length - 1);
 | 
			
		||||
            },
 | 
			
		||||
            onInput: (value) =>
 | 
			
		||||
              setTempToastConfig({ ...getTempToastConfig(), prefix: value }),
 | 
			
		||||
            onFocusChanged: () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,19 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Example CLI application demonstrating the ccCLI framework
 | 
			
		||||
 * This example shows how to create a calculator CLI with global context injection
 | 
			
		||||
 * and ChatManager integration for Minecraft chat functionality
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Command, createCli, CliError } from "../lib/ccCLI/index";
 | 
			
		||||
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
 | 
			
		||||
import { ChatManager, ChatMessage, ChatToast } from "../lib/ChatManager";
 | 
			
		||||
 | 
			
		||||
// 1. Define global context type
 | 
			
		||||
interface AppContext {
 | 
			
		||||
  appName: string;
 | 
			
		||||
  log: (message: string) => void;
 | 
			
		||||
  debugMode: boolean;
 | 
			
		||||
  chatManager?: ChatManager;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 2. Define individual commands
 | 
			
		||||
@@ -153,10 +156,346 @@ const configCommand: Command<AppContext> = {
 | 
			
		||||
  ]),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// ChatManager commands
 | 
			
		||||
const chatSendCommand: Command<AppContext> = {
 | 
			
		||||
  name: "send",
 | 
			
		||||
  description: "Send a chat message",
 | 
			
		||||
  args: [
 | 
			
		||||
    { name: "message", description: "The message to send", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  options: new Map([
 | 
			
		||||
    [
 | 
			
		||||
      "player",
 | 
			
		||||
      {
 | 
			
		||||
        name: "player",
 | 
			
		||||
        shortName: "p",
 | 
			
		||||
        description: "Target player for private message",
 | 
			
		||||
        defaultValue: undefined,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      "prefix",
 | 
			
		||||
      {
 | 
			
		||||
        name: "prefix",
 | 
			
		||||
        description: "Message prefix",
 | 
			
		||||
        defaultValue: "CC",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ args, options, context }): Result<void, CliError> => {
 | 
			
		||||
    if (!context.chatManager) {
 | 
			
		||||
      print(
 | 
			
		||||
        "Error: ChatManager not initialized. No chatbox peripherals found.",
 | 
			
		||||
      );
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const message: ChatMessage = {
 | 
			
		||||
      message: args.message as string,
 | 
			
		||||
      targetPlayer: options.player as string | undefined,
 | 
			
		||||
      prefix: options.prefix as string,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const result = context.chatManager.sendMessage(message);
 | 
			
		||||
    if (result.isOk()) {
 | 
			
		||||
      print(`Message queued: "${String(args.message)}"`);
 | 
			
		||||
 | 
			
		||||
      const targetPlayer = options.player;
 | 
			
		||||
      if (
 | 
			
		||||
        targetPlayer !== undefined &&
 | 
			
		||||
        targetPlayer !== null &&
 | 
			
		||||
        typeof targetPlayer === "string"
 | 
			
		||||
      ) {
 | 
			
		||||
        print(`Target: ${targetPlayer}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        print("Target: Global chat");
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      print(`Failed to queue message: ${result.error.reason}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const chatToastCommand: Command<AppContext> = {
 | 
			
		||||
  name: "toast",
 | 
			
		||||
  description: "Send a toast notification to a player",
 | 
			
		||||
  args: [
 | 
			
		||||
    { name: "player", description: "Target player username", required: true },
 | 
			
		||||
    { name: "title", description: "Toast title", required: true },
 | 
			
		||||
    { name: "message", description: "Toast message", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  options: new Map([
 | 
			
		||||
    [
 | 
			
		||||
      "prefix",
 | 
			
		||||
      {
 | 
			
		||||
        name: "prefix",
 | 
			
		||||
        description: "Message prefix",
 | 
			
		||||
        defaultValue: "CC",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ args, options, context }): Result<void, CliError> => {
 | 
			
		||||
    if (!context.chatManager) {
 | 
			
		||||
      print(
 | 
			
		||||
        "Error: ChatManager not initialized. No chatbox peripherals found.",
 | 
			
		||||
      );
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const toast: ChatToast = {
 | 
			
		||||
      targetPlayer: args.player as string,
 | 
			
		||||
      title: args.title as string,
 | 
			
		||||
      message: args.message as string,
 | 
			
		||||
      prefix: options.prefix as string,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const result = context.chatManager.sendToast(toast);
 | 
			
		||||
    if (result.isOk()) {
 | 
			
		||||
      print(
 | 
			
		||||
        `Toast queued for ${String(args.player)}: "${String(args.title)}" - "${String(args.message)}"`,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      print(`Failed to queue toast: ${result.error.reason}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const chatStatusCommand: Command<AppContext> = {
 | 
			
		||||
  name: "status",
 | 
			
		||||
  description: "Show ChatManager status and queue information",
 | 
			
		||||
  action: ({ context }): Result<void, CliError> => {
 | 
			
		||||
    if (!context.chatManager) {
 | 
			
		||||
      print("ChatManager: Not initialized (no chatbox peripherals found)");
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print("=== ChatManager Status ===");
 | 
			
		||||
    print(`Pending messages: ${context.chatManager.getPendingMessageCount()}`);
 | 
			
		||||
    print(`Pending toasts: ${context.chatManager.getPendingToastCount()}`);
 | 
			
		||||
    print(
 | 
			
		||||
      `Buffered received: ${context.chatManager.getBufferedMessageCount()}`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const chatboxStatus = context.chatManager.getChatboxStatus();
 | 
			
		||||
    print(`Chatboxes: ${chatboxStatus.length} total`);
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < chatboxStatus.length; i++) {
 | 
			
		||||
      const status = chatboxStatus[i] ? "idle" : "busy";
 | 
			
		||||
      print(`  Chatbox ${i + 1}: ${status}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const chatReceiveCommand: Command<AppContext> = {
 | 
			
		||||
  name: "receive",
 | 
			
		||||
  description: "Check for received chat messages",
 | 
			
		||||
  options: new Map([
 | 
			
		||||
    [
 | 
			
		||||
      "count",
 | 
			
		||||
      {
 | 
			
		||||
        name: "count",
 | 
			
		||||
        shortName: "c",
 | 
			
		||||
        description: "Number of messages to retrieve",
 | 
			
		||||
        defaultValue: 1,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ options, context }): Result<void, CliError> => {
 | 
			
		||||
    if (!context.chatManager) {
 | 
			
		||||
      print(
 | 
			
		||||
        "Error: ChatManager not initialized. No chatbox peripherals found.",
 | 
			
		||||
      );
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const count = tonumber(options.count as string) ?? 1;
 | 
			
		||||
    let retrieved = 0;
 | 
			
		||||
 | 
			
		||||
    print("=== Received Messages ===");
 | 
			
		||||
    for (let i = 0; i < count; i++) {
 | 
			
		||||
      const result = context.chatManager.getReceivedMessage();
 | 
			
		||||
      if (result.isOk()) {
 | 
			
		||||
        const event = result.value;
 | 
			
		||||
        print(`[${event.username}]: ${event.message}`);
 | 
			
		||||
        if (event.uuid !== undefined) {
 | 
			
		||||
          print(`  UUID: ${event.uuid}`);
 | 
			
		||||
        }
 | 
			
		||||
        retrieved++;
 | 
			
		||||
      } else {
 | 
			
		||||
        // Buffer is empty
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (retrieved === 0) {
 | 
			
		||||
      print("No messages in buffer");
 | 
			
		||||
    } else {
 | 
			
		||||
      print(`Retrieved ${retrieved} message(s)`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const chatSendImmediateCommand: Command<AppContext> = {
 | 
			
		||||
  name: "send-immediate",
 | 
			
		||||
  description: "Send a chat message immediately (bypass queue)",
 | 
			
		||||
  args: [
 | 
			
		||||
    { name: "message", description: "The message to send", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  options: new Map([
 | 
			
		||||
    [
 | 
			
		||||
      "player",
 | 
			
		||||
      {
 | 
			
		||||
        name: "player",
 | 
			
		||||
        shortName: "p",
 | 
			
		||||
        description: "Target player for private message",
 | 
			
		||||
        defaultValue: undefined,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      "prefix",
 | 
			
		||||
      {
 | 
			
		||||
        name: "prefix",
 | 
			
		||||
        description: "Message prefix",
 | 
			
		||||
        defaultValue: "CC",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ args, options, context }): Result<void, CliError> => {
 | 
			
		||||
    if (!context.chatManager) {
 | 
			
		||||
      print(
 | 
			
		||||
        "Error: ChatManager not initialized. No chatbox peripherals found.",
 | 
			
		||||
      );
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const message: ChatMessage = {
 | 
			
		||||
      message: args.message as string,
 | 
			
		||||
      targetPlayer: options.player as string | undefined,
 | 
			
		||||
      prefix: options.prefix as string,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const result = context.chatManager.sendMessageImmediate(message);
 | 
			
		||||
    if (result.isOk()) {
 | 
			
		||||
      print(`Message sent immediately: "${String(args.message)}"`);
 | 
			
		||||
    } else {
 | 
			
		||||
      print(`Failed to send message: ${result.error.reason}`);
 | 
			
		||||
      if (result.error.kind === "NoIdleChatbox") {
 | 
			
		||||
        print("All chatboxes are currently busy. Try queuing instead.");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const chatStopCommand: Command<AppContext> = {
 | 
			
		||||
  name: "stop",
 | 
			
		||||
  description: "Stop the ChatManager",
 | 
			
		||||
  action: ({ context }): Result<void, CliError> => {
 | 
			
		||||
    if (!context.chatManager) {
 | 
			
		||||
      print("Error: ChatManager not initialized.");
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const result = context.chatManager.stop();
 | 
			
		||||
    if (result.isOk()) {
 | 
			
		||||
      print("ChatManager stopped successfully.");
 | 
			
		||||
    } else {
 | 
			
		||||
      print(`Failed to stop ChatManager: ${result.error.reason}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const chatClearCommand: Command<AppContext> = {
 | 
			
		||||
  name: "clear",
 | 
			
		||||
  description: "Clear queues and buffer",
 | 
			
		||||
  options: new Map([
 | 
			
		||||
    [
 | 
			
		||||
      "queues",
 | 
			
		||||
      {
 | 
			
		||||
        name: "queues",
 | 
			
		||||
        shortName: "q",
 | 
			
		||||
        description: "Clear message and toast queues",
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      "buffer",
 | 
			
		||||
      {
 | 
			
		||||
        name: "buffer",
 | 
			
		||||
        shortName: "b",
 | 
			
		||||
        description: "Clear received message buffer",
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ options, context }): Result<void, CliError> => {
 | 
			
		||||
    if (!context.chatManager) {
 | 
			
		||||
      print("Error: ChatManager not initialized.");
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const clearQueues = options.queues as boolean;
 | 
			
		||||
    const clearBuffer = options.buffer as boolean;
 | 
			
		||||
 | 
			
		||||
    if (!clearQueues && !clearBuffer) {
 | 
			
		||||
      print("Specify --queues or --buffer (or both) to clear.");
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const results: string[] = [];
 | 
			
		||||
 | 
			
		||||
    if (clearQueues) {
 | 
			
		||||
      const result = context.chatManager.clearQueues();
 | 
			
		||||
      if (result.isOk()) {
 | 
			
		||||
        results.push("Queues cleared");
 | 
			
		||||
      } else {
 | 
			
		||||
        results.push(`Failed to clear queues: ${result.error.reason}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (clearBuffer) {
 | 
			
		||||
      const result = context.chatManager.clearBuffer();
 | 
			
		||||
      if (result.isOk()) {
 | 
			
		||||
        results.push("Buffer cleared");
 | 
			
		||||
      } else {
 | 
			
		||||
        results.push(`Failed to clear buffer: ${result.error.reason}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    results.forEach((msg) => print(msg));
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const chatCommand: Command<AppContext> = {
 | 
			
		||||
  name: "chat",
 | 
			
		||||
  description: "Chat management commands using ChatManager",
 | 
			
		||||
  subcommands: new Map([
 | 
			
		||||
    ["send", chatSendCommand],
 | 
			
		||||
    ["send-immediate", chatSendImmediateCommand],
 | 
			
		||||
    ["toast", chatToastCommand],
 | 
			
		||||
    ["status", chatStatusCommand],
 | 
			
		||||
    ["receive", chatReceiveCommand],
 | 
			
		||||
    ["stop", chatStopCommand],
 | 
			
		||||
    ["clear", chatClearCommand],
 | 
			
		||||
  ]),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 3. Define root command
 | 
			
		||||
const rootCommand: Command<AppContext> = {
 | 
			
		||||
  name: "calculator",
 | 
			
		||||
  description: "A feature-rich calculator program",
 | 
			
		||||
  description: "A feature-rich calculator and chat management program",
 | 
			
		||||
  options: new Map([
 | 
			
		||||
    [
 | 
			
		||||
      "debug",
 | 
			
		||||
@@ -172,6 +511,7 @@ const rootCommand: Command<AppContext> = {
 | 
			
		||||
    ["math", mathCommand],
 | 
			
		||||
    ["greet", greetCommand],
 | 
			
		||||
    ["config", configCommand],
 | 
			
		||||
    ["chat", chatCommand],
 | 
			
		||||
  ]),
 | 
			
		||||
  action: ({ options, context }): Result<void, CliError> => {
 | 
			
		||||
    // Update debug mode from command line option
 | 
			
		||||
@@ -183,20 +523,58 @@ const rootCommand: Command<AppContext> = {
 | 
			
		||||
 | 
			
		||||
    print(`Welcome to ${context.appName}!`);
 | 
			
		||||
    print("Use --help to see available commands");
 | 
			
		||||
 | 
			
		||||
    if (context.chatManager) {
 | 
			
		||||
      print("ChatManager initialized and ready!");
 | 
			
		||||
    } else {
 | 
			
		||||
      print("Note: No chatbox peripherals found - chat commands unavailable");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 4. Create global context instance
 | 
			
		||||
// 4. Initialize ChatManager if chatbox peripherals are available
 | 
			
		||||
function initializeChatManager(): ChatManager | undefined {
 | 
			
		||||
  // Find all available chatbox peripherals
 | 
			
		||||
  const peripheralNames = peripheral.getNames();
 | 
			
		||||
  const chatboxPeripherals: ChatBoxPeripheral[] = [];
 | 
			
		||||
 | 
			
		||||
  for (const name of peripheralNames) {
 | 
			
		||||
    const peripheralType = peripheral.getType(name);
 | 
			
		||||
    if (peripheralType[0] === "chatBox") {
 | 
			
		||||
      const chatbox = peripheral.wrap(name) as ChatBoxPeripheral;
 | 
			
		||||
      chatboxPeripherals.push(chatbox);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (chatboxPeripherals.length === 0) {
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const chatManager = new ChatManager(chatboxPeripherals);
 | 
			
		||||
 | 
			
		||||
  // Start ChatManager in async mode so it doesn't block the CLI
 | 
			
		||||
  const runResult = chatManager.runAsync();
 | 
			
		||||
  if (runResult.isErr()) {
 | 
			
		||||
    print(`Warning: Failed to start ChatManager: ${runResult.error.reason}`);
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return chatManager;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 5. Create global context instance
 | 
			
		||||
const appContext: AppContext = {
 | 
			
		||||
  appName: "MyAwesomeCalculator",
 | 
			
		||||
  appName: "MyAwesome Calculator & Chat Manager",
 | 
			
		||||
  debugMode: false,
 | 
			
		||||
  log: (message) => {
 | 
			
		||||
    print(`[LOG] ${message}`);
 | 
			
		||||
  },
 | 
			
		||||
  chatManager: initializeChatManager(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 5. Create and export CLI handler
 | 
			
		||||
// 6. Create and export CLI handler
 | 
			
		||||
const cli = createCli(rootCommand, { globalContext: appContext });
 | 
			
		||||
const args = [...$vararg];
 | 
			
		||||
cli(args);
 | 
			
		||||
@@ -215,11 +593,24 @@ cli(['greet', '-n', 'World', '-t', '3']); // Output: Hello, World! (3 times)
 | 
			
		||||
cli(['config', 'show']);                 // Shows current config
 | 
			
		||||
cli(['config', 'set', 'theme', 'dark']); // Sets config
 | 
			
		||||
 | 
			
		||||
// Chat management (requires chatbox peripherals)
 | 
			
		||||
cli(['chat', 'status']);                 // Shows ChatManager status
 | 
			
		||||
cli(['chat', 'send', 'Hello World!']);   // Sends global message (queued)
 | 
			
		||||
cli(['chat', 'send', 'Hi there!', '--player', 'Steve']); // Private message (queued)
 | 
			
		||||
cli(['chat', 'send-immediate', 'Urgent!', '--player', 'Admin']); // Immediate send
 | 
			
		||||
cli(['chat', 'toast', 'Steve', 'Alert', 'Server restart in 5 minutes']); // Toast notification
 | 
			
		||||
cli(['chat', 'receive', '--count', '5']); // Check for received messages
 | 
			
		||||
cli(['chat', 'clear', '--queues']);      // Clear pending queues
 | 
			
		||||
cli(['chat', 'clear', '--buffer']);      // Clear received buffer
 | 
			
		||||
cli(['chat', 'stop']);                   // Stop ChatManager
 | 
			
		||||
 | 
			
		||||
// Help examples
 | 
			
		||||
cli(['--help']);                         // Shows root help
 | 
			
		||||
cli(['math', '--help']);                 // Shows math command help
 | 
			
		||||
cli(['config', 'set', '--help']);       // Shows config set help
 | 
			
		||||
cli(['chat', '--help']);                 // Shows chat command help
 | 
			
		||||
cli(['chat', 'send', '--help']);         // Shows chat send help
 | 
			
		||||
 | 
			
		||||
// Debug mode
 | 
			
		||||
cli(['--debug', 'math', 'add', '1', '2']); // Enables debug logging
 | 
			
		||||
cli(['--debug', 'chat', 'status']);      // Debug mode with chat status
 | 
			
		||||
*/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										657
									
								
								src/lib/ChatManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										657
									
								
								src/lib/ChatManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,657 @@
 | 
			
		||||
import { Queue } from "./datatype/Queue";
 | 
			
		||||
import { ChatBoxEvent, pullEventAs } from "./event";
 | 
			
		||||
import { Result, Ok, Err } from "./thirdparty/ts-result-es";
 | 
			
		||||
import { gTimerManager } from "./TimerManager";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chat manager error types
 | 
			
		||||
 */
 | 
			
		||||
export interface ChatManagerError {
 | 
			
		||||
  kind: "ChatManager";
 | 
			
		||||
  reason: string;
 | 
			
		||||
  chatboxIndex?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NoIdleChatboxError {
 | 
			
		||||
  kind: "NoIdleChatbox";
 | 
			
		||||
  reason: "All chatboxes are busy";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SendFailureError {
 | 
			
		||||
  kind: "SendFailure";
 | 
			
		||||
  reason: string;
 | 
			
		||||
  chatboxIndex: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EmptyBufferError {
 | 
			
		||||
  kind: "EmptyBuffer";
 | 
			
		||||
  reason: "No messages in buffer";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ChatError =
 | 
			
		||||
  | ChatManagerError
 | 
			
		||||
  | NoIdleChatboxError
 | 
			
		||||
  | SendFailureError
 | 
			
		||||
  | EmptyBufferError;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base interface for chat messages and toasts
 | 
			
		||||
 */
 | 
			
		||||
interface ChatBasicMessage {
 | 
			
		||||
  message: string | MinecraftTextComponent | MinecraftTextComponent[];
 | 
			
		||||
  prefix?: string;
 | 
			
		||||
  brackets?: string;
 | 
			
		||||
  bracketColor?: string;
 | 
			
		||||
  range?: number;
 | 
			
		||||
  utf8Support?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface for chat toast notifications
 | 
			
		||||
 */
 | 
			
		||||
export interface ChatToast extends ChatBasicMessage {
 | 
			
		||||
  /** Target player username to send the toast to */
 | 
			
		||||
  targetPlayer: string;
 | 
			
		||||
  /** Title of the toast notification */
 | 
			
		||||
  title: string | MinecraftTextComponent | MinecraftTextComponent[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface for regular chat messages
 | 
			
		||||
 */
 | 
			
		||||
export interface ChatMessage extends ChatBasicMessage {
 | 
			
		||||
  /** Optional target player username for private messages */
 | 
			
		||||
  targetPlayer?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * ChatManager class for managing multiple ChatBox peripherals
 | 
			
		||||
 * Handles message queuing, sending with cooldown management, and event receiving
 | 
			
		||||
 * Uses Result types for robust error handling
 | 
			
		||||
 */
 | 
			
		||||
export class ChatManager {
 | 
			
		||||
  /** Array of all available ChatBox peripherals */
 | 
			
		||||
  private chatboxes: ChatBoxPeripheral[];
 | 
			
		||||
 | 
			
		||||
  /** Queue for pending chat messages */
 | 
			
		||||
  private messageQueue = new Queue<ChatMessage>();
 | 
			
		||||
 | 
			
		||||
  /** Queue for pending toast notifications */
 | 
			
		||||
  private toastQueue = new Queue<ChatToast>();
 | 
			
		||||
 | 
			
		||||
  /** Buffer for received chat events */
 | 
			
		||||
  private chatBuffer = new Queue<ChatBoxEvent>();
 | 
			
		||||
 | 
			
		||||
  /** Array tracking which chatboxes are currently idle (not in cooldown) */
 | 
			
		||||
  private idleChatboxes: boolean[];
 | 
			
		||||
 | 
			
		||||
  /** Flag
 | 
			
		||||
 to control the running state of loops */
 | 
			
		||||
  private isRunning = false;
 | 
			
		||||
 | 
			
		||||
  /** Lua thread for managing chat operations */
 | 
			
		||||
  private thread?: LuaThread;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructor - initializes the ChatManager with available ChatBox peripherals
 | 
			
		||||
   * @param peripherals Array of ChatBox peripherals to manage
 | 
			
		||||
   */
 | 
			
		||||
  constructor(peripherals: ChatBoxPeripheral[]) {
 | 
			
		||||
    if (peripherals.length === 0) {
 | 
			
		||||
      throw new Error("ChatManager requires at least one ChatBox peripheral");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chatboxes = peripherals;
 | 
			
		||||
    // Initially all chatboxes are idle
 | 
			
		||||
    this.idleChatboxes = peripherals.map(() => true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds a chat message to the sending queue
 | 
			
		||||
   * @param message The chat message to send
 | 
			
		||||
   * @returns Result indicating success or failure
 | 
			
		||||
   */
 | 
			
		||||
  public sendMessage(message: ChatMessage): Result<void, ChatManagerError> {
 | 
			
		||||
    try {
 | 
			
		||||
      this.messageQueue.enqueue(message);
 | 
			
		||||
      return new Ok(undefined);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: `Failed to enqueue message: ${String(error)}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds a toast notification to the sending queue
 | 
			
		||||
   * @param toast The toast notification to send
 | 
			
		||||
   * @returns Result indicating success or failure
 | 
			
		||||
   */
 | 
			
		||||
  public sendToast(toast: ChatToast): Result<void, ChatManagerError> {
 | 
			
		||||
    try {
 | 
			
		||||
      this.toastQueue.enqueue(toast);
 | 
			
		||||
      return new Ok(undefined);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: `Failed to enqueue toast: ${String(error)}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Retrieves and removes the next received chat event from the buffer
 | 
			
		||||
   * @returns Result containing the chat event or an error if buffer is empty
 | 
			
		||||
   */
 | 
			
		||||
  public getReceivedMessage(): Result<ChatBoxEvent, EmptyBufferError> {
 | 
			
		||||
    const event = this.chatBuffer.dequeue();
 | 
			
		||||
    if (event === undefined) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "EmptyBuffer",
 | 
			
		||||
        reason: "No messages in buffer",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return new Ok(event);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Finds the first available (idle) chatbox
 | 
			
		||||
   * @returns Result containing chatbox index or error if none available
 | 
			
		||||
   */
 | 
			
		||||
  private findIdleChatbox(): Result<number, NoIdleChatboxError> {
 | 
			
		||||
    for (let i = 0; i < this.idleChatboxes.length; i++) {
 | 
			
		||||
      if (this.idleChatboxes[i]) {
 | 
			
		||||
        return new Ok(i);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return new Err({
 | 
			
		||||
      kind: "NoIdleChatbox",
 | 
			
		||||
      reason: "All chatboxes are busy",
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Marks a chatbox as busy and sets up a timer to mark it as idle after cooldown
 | 
			
		||||
   * @param chatboxIndex Index of the chatbox to mark as busy
 | 
			
		||||
   * @returns Result indicating success or failure
 | 
			
		||||
   */
 | 
			
		||||
  private setChatboxBusy(chatboxIndex: number): Result<void, ChatManagerError> {
 | 
			
		||||
    if (chatboxIndex < 0 || chatboxIndex >= this.idleChatboxes.length) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: "Invalid chatbox index",
 | 
			
		||||
        chatboxIndex,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.idleChatboxes[chatboxIndex] = false;
 | 
			
		||||
 | 
			
		||||
    if (!gTimerManager.status()) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: "TimerManager is not running",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    gTimerManager.setTimeOut(1, () => {
 | 
			
		||||
      this.idleChatboxes[chatboxIndex] = true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Attempts to send a chat message using an available chatbox
 | 
			
		||||
   * @param message The message to send
 | 
			
		||||
   * @returns Result indicating success or failure with error details
 | 
			
		||||
   */
 | 
			
		||||
  private trySendMessage(message: ChatMessage): Result<void, ChatError> {
 | 
			
		||||
    const chatboxResult = this.findIdleChatbox();
 | 
			
		||||
    if (chatboxResult.isErr()) {
 | 
			
		||||
      return chatboxResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const chatboxIndex = chatboxResult.value;
 | 
			
		||||
    const chatbox = this.chatboxes[chatboxIndex];
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let success: boolean;
 | 
			
		||||
      let errorMsg: string | undefined;
 | 
			
		||||
 | 
			
		||||
      // Determine the appropriate sending method based on message properties
 | 
			
		||||
      if (message.targetPlayer !== undefined) {
 | 
			
		||||
        // Send private message to specific player
 | 
			
		||||
        if (typeof message.message === "string") {
 | 
			
		||||
          [success, errorMsg] = chatbox.sendMessageToPlayer(
 | 
			
		||||
            textutils.serialiseJSON(message.message, {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
            }),
 | 
			
		||||
            message.targetPlayer,
 | 
			
		||||
            textutils.serialiseJSON(message.prefix ?? "AP", {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
            }),
 | 
			
		||||
            message.brackets,
 | 
			
		||||
            message.bracketColor,
 | 
			
		||||
            message.range,
 | 
			
		||||
            message.utf8Support,
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          // Handle MinecraftTextComponent for private message
 | 
			
		||||
          [success, errorMsg] = chatbox.sendFormattedMessageToPlayer(
 | 
			
		||||
            textutils.serialiseJSON(message.message, {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
              allow_repetitions: true,
 | 
			
		||||
            }),
 | 
			
		||||
            message.targetPlayer,
 | 
			
		||||
            textutils.serialiseJSON(message.prefix ?? "AP", {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
            }),
 | 
			
		||||
            message.brackets,
 | 
			
		||||
            message.bracketColor,
 | 
			
		||||
            message.range,
 | 
			
		||||
            message.utf8Support,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // Send global message
 | 
			
		||||
        if (typeof message.message === "string") {
 | 
			
		||||
          [success, errorMsg] = chatbox.sendMessage(
 | 
			
		||||
            textutils.serialiseJSON(message.message, {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
            }),
 | 
			
		||||
            textutils.serialiseJSON(message.prefix ?? "AP", {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
            }),
 | 
			
		||||
            message.brackets,
 | 
			
		||||
            message.bracketColor,
 | 
			
		||||
            message.range,
 | 
			
		||||
            message.utf8Support,
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          // Handle MinecraftTextComponent for global message
 | 
			
		||||
          [success, errorMsg] = chatbox.sendFormattedMessage(
 | 
			
		||||
            textutils.serialiseJSON(message.message, {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
              allow_repetitions: true,
 | 
			
		||||
            }),
 | 
			
		||||
            textutils.serialiseJSON(message.prefix ?? "AP", {
 | 
			
		||||
              unicode_strings: message.utf8Support,
 | 
			
		||||
            }),
 | 
			
		||||
            message.brackets,
 | 
			
		||||
            message.bracketColor,
 | 
			
		||||
            message.range,
 | 
			
		||||
            message.utf8Support,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (success) {
 | 
			
		||||
        // Mark chatbox as busy for cooldown period
 | 
			
		||||
        const busyResult = this.setChatboxBusy(chatboxIndex);
 | 
			
		||||
        if (busyResult.isErr()) {
 | 
			
		||||
          return busyResult;
 | 
			
		||||
        }
 | 
			
		||||
        return new Ok(undefined);
 | 
			
		||||
      } else {
 | 
			
		||||
        return new Err({
 | 
			
		||||
          kind: "SendFailure",
 | 
			
		||||
          reason: errorMsg ?? "Unknown send failure",
 | 
			
		||||
          chatboxIndex,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "SendFailure",
 | 
			
		||||
        reason: `Exception during send: ${String(error)}`,
 | 
			
		||||
        chatboxIndex,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Attempts to send a toast notification using an available chatbox
 | 
			
		||||
   * @param toast The toast to send
 | 
			
		||||
   * @returns Result indicating success or failure with error details
 | 
			
		||||
   */
 | 
			
		||||
  private trySendToast(toast: ChatToast): Result<void, ChatError> {
 | 
			
		||||
    const chatboxResult = this.findIdleChatbox();
 | 
			
		||||
    if (chatboxResult.isErr()) {
 | 
			
		||||
      return chatboxResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const chatboxIndex = chatboxResult.value;
 | 
			
		||||
    const chatbox = this.chatboxes[chatboxIndex];
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let success: boolean;
 | 
			
		||||
      let errorMsg: string | undefined;
 | 
			
		||||
 | 
			
		||||
      // Send toast notification
 | 
			
		||||
      if (
 | 
			
		||||
        typeof toast.message === "string" &&
 | 
			
		||||
        typeof toast.title === "string"
 | 
			
		||||
      ) {
 | 
			
		||||
        [success, errorMsg] = chatbox.sendToastToPlayer(
 | 
			
		||||
          textutils.serialiseJSON(toast.message, {
 | 
			
		||||
            unicode_strings: toast.utf8Support,
 | 
			
		||||
          }),
 | 
			
		||||
          textutils.serialiseJSON(toast.title, {
 | 
			
		||||
            unicode_strings: toast.utf8Support,
 | 
			
		||||
          }),
 | 
			
		||||
          toast.targetPlayer,
 | 
			
		||||
          textutils.serialiseJSON(toast.prefix ?? "AP", {
 | 
			
		||||
            unicode_strings: toast.utf8Support,
 | 
			
		||||
          }),
 | 
			
		||||
          toast.brackets,
 | 
			
		||||
          toast.bracketColor,
 | 
			
		||||
          toast.range,
 | 
			
		||||
          toast.utf8Support,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        // Handle MinecraftTextComponent for toast
 | 
			
		||||
        const messageJson =
 | 
			
		||||
          typeof toast.message === "string"
 | 
			
		||||
            ? toast.message
 | 
			
		||||
            : textutils.serialiseJSON(toast.message, {
 | 
			
		||||
                unicode_strings: true,
 | 
			
		||||
                allow_repetitions: toast.utf8Support,
 | 
			
		||||
              });
 | 
			
		||||
        const titleJson =
 | 
			
		||||
          typeof toast.title === "string"
 | 
			
		||||
            ? toast.title
 | 
			
		||||
            : textutils.serialiseJSON(toast.title, {
 | 
			
		||||
                unicode_strings: true,
 | 
			
		||||
                allow_repetitions: toast.utf8Support,
 | 
			
		||||
              });
 | 
			
		||||
 | 
			
		||||
        [success, errorMsg] = chatbox.sendFormattedToastToPlayer(
 | 
			
		||||
          messageJson,
 | 
			
		||||
          titleJson,
 | 
			
		||||
          toast.targetPlayer,
 | 
			
		||||
          textutils.serialiseJSON(toast.prefix ?? "AP", {
 | 
			
		||||
            unicode_strings: toast.utf8Support,
 | 
			
		||||
          }),
 | 
			
		||||
          toast.brackets,
 | 
			
		||||
          toast.bracketColor,
 | 
			
		||||
          toast.range,
 | 
			
		||||
          toast.utf8Support,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (success) {
 | 
			
		||||
        // Mark chatbox as busy for cooldown period
 | 
			
		||||
        const busyResult = this.setChatboxBusy(chatboxIndex);
 | 
			
		||||
        if (busyResult.isErr()) {
 | 
			
		||||
          return busyResult;
 | 
			
		||||
        }
 | 
			
		||||
        return new Ok(undefined);
 | 
			
		||||
      } else {
 | 
			
		||||
        return new Err({
 | 
			
		||||
          kind: "SendFailure",
 | 
			
		||||
          reason: errorMsg ?? "Unknown toast send failure",
 | 
			
		||||
          chatboxIndex,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "SendFailure",
 | 
			
		||||
        reason: `Exception during toast send: ${String(error)}`,
 | 
			
		||||
        chatboxIndex,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Main sending loop - continuously processes message and toast queues
 | 
			
		||||
   * Runs in a separate coroutine to handle sending with proper timing
 | 
			
		||||
   */
 | 
			
		||||
  private sendLoop(): void {
 | 
			
		||||
    while (this.isRunning) {
 | 
			
		||||
      let sentSomething = false;
 | 
			
		||||
 | 
			
		||||
      // Try to send a message if queue is not empty
 | 
			
		||||
      if (this.messageQueue.size() > 0) {
 | 
			
		||||
        const message = this.messageQueue.peek();
 | 
			
		||||
        if (message) {
 | 
			
		||||
          const result = this.trySendMessage(message);
 | 
			
		||||
          if (result.isOk()) {
 | 
			
		||||
            this.messageQueue.dequeue(); // Remove from queue only if successfully sent
 | 
			
		||||
            sentSomething = true;
 | 
			
		||||
          } else if (result.error.kind === "SendFailure") {
 | 
			
		||||
            // Log send failures but keep trying
 | 
			
		||||
            print(`Failed to send message: ${result.error.reason}`);
 | 
			
		||||
            this.messageQueue.dequeue(); // Remove failed message to prevent infinite retry
 | 
			
		||||
          }
 | 
			
		||||
          // For NoIdleChatbox errors, we keep the message in queue and try again later
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Try to send a toast if queue is not empty
 | 
			
		||||
      if (this.toastQueue.size() > 0) {
 | 
			
		||||
        const toast = this.toastQueue.peek();
 | 
			
		||||
        if (toast) {
 | 
			
		||||
          const result = this.trySendToast(toast);
 | 
			
		||||
          if (result.isOk()) {
 | 
			
		||||
            this.toastQueue.dequeue(); // Remove from queue only if successfully sent
 | 
			
		||||
            sentSomething = true;
 | 
			
		||||
          } else if (result.error.kind === "SendFailure") {
 | 
			
		||||
            // Log send failures but keep trying
 | 
			
		||||
            print(`Failed to send toast: ${result.error.reason}`);
 | 
			
		||||
            this.toastQueue.dequeue(); // Remove failed toast to prevent infinite retry
 | 
			
		||||
          }
 | 
			
		||||
          // For NoIdleChatbox errors, we keep the toast in queue and try again later
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Small sleep to prevent busy waiting and allow other coroutines to run
 | 
			
		||||
      if (!sentSomething) {
 | 
			
		||||
        sleep(0.1);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Main receiving loop - continuously listens for chat events
 | 
			
		||||
   * Runs in a separate coroutine to handle incoming messages
 | 
			
		||||
   */
 | 
			
		||||
  private receiveLoop(): void {
 | 
			
		||||
    while (this.isRunning) {
 | 
			
		||||
      try {
 | 
			
		||||
        // Listen for chatbox_message events
 | 
			
		||||
        const event = pullEventAs(ChatBoxEvent, "chat");
 | 
			
		||||
 | 
			
		||||
        if (event) {
 | 
			
		||||
          // Store received event in buffer for user processing
 | 
			
		||||
          this.chatBuffer.enqueue(event);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        // Log receive errors but continue running
 | 
			
		||||
        print(`Error in receive loop: ${String(error)}`);
 | 
			
		||||
        sleep(0.1); // Brief pause before retrying
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Starts the ChatManager's main operation loops
 | 
			
		||||
   * Launches both sending and receiving coroutines in parallel
 | 
			
		||||
   * @returns Result indicating success or failure of startup
 | 
			
		||||
   */
 | 
			
		||||
  public run(): Result<void, ChatManagerError> {
 | 
			
		||||
    if (this.isRunning) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: "ChatManager is already running",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      this.isRunning = true;
 | 
			
		||||
 | 
			
		||||
      // Start both send and receive loops in parallel
 | 
			
		||||
      parallel.waitForAll(
 | 
			
		||||
        () => this.sendLoop(),
 | 
			
		||||
        () => this.receiveLoop(),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return new Ok(undefined);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.isRunning = false;
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: `Failed to start ChatManager: ${String(error)}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Starts the ChatManager asynchronously without blocking
 | 
			
		||||
   * Useful when you need to run other code alongside the ChatManager
 | 
			
		||||
   * @returns Result indicating success or failure of async startup
 | 
			
		||||
   */
 | 
			
		||||
  public runAsync(): Result<LuaThread, ChatManagerError> {
 | 
			
		||||
    if (this.isRunning) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: "ChatManager is already running",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      this.isRunning = true;
 | 
			
		||||
      this.thread = coroutine.create(() => {
 | 
			
		||||
        const result = this.run();
 | 
			
		||||
        if (result.isErr()) {
 | 
			
		||||
          print(`ChatManager async error: ${result.error.reason}`);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Start the run method in a separate coroutine
 | 
			
		||||
      coroutine.resume(this.thread);
 | 
			
		||||
 | 
			
		||||
      return new Ok(this.thread);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.isRunning = false;
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: `Failed to start ChatManager async: ${String(error)}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stops the ChatManager loops gracefully
 | 
			
		||||
   * @returns Result indicating success or failure of shutdown
 | 
			
		||||
   */
 | 
			
		||||
  public stop(): Result<void, ChatManagerError> {
 | 
			
		||||
    if (!this.isRunning) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: "ChatManager is not running",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      this.isRunning = false;
 | 
			
		||||
      return new Ok(undefined);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: `Failed to stop ChatManager: ${String(error)}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the number of pending messages in the queue
 | 
			
		||||
   * @returns Number of pending messages
 | 
			
		||||
   */
 | 
			
		||||
  public getPendingMessageCount(): number {
 | 
			
		||||
    return this.messageQueue.size();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the number of pending toasts in the queue
 | 
			
		||||
   * @returns Number of pending toasts
 | 
			
		||||
   */
 | 
			
		||||
  public getPendingToastCount(): number {
 | 
			
		||||
    return this.toastQueue.size();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the number of received messages in the buffer
 | 
			
		||||
   * @returns Number of buffered received messages
 | 
			
		||||
   */
 | 
			
		||||
  public getBufferedMessageCount(): number {
 | 
			
		||||
    return this.chatBuffer.size();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the current status of all chatboxes
 | 
			
		||||
   * @returns Array of boolean values indicating which chatboxes are idle
 | 
			
		||||
   */
 | 
			
		||||
  public getChatboxStatus(): boolean[] {
 | 
			
		||||
    return [...this.idleChatboxes]; // Return a copy to prevent external modification
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the running state of the ChatManager
 | 
			
		||||
   * @returns true if ChatManager is currently running
 | 
			
		||||
   */
 | 
			
		||||
  public isManagerRunning(): boolean {
 | 
			
		||||
    return this.isRunning;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Clears all pending messages and toasts from queues
 | 
			
		||||
   * Does not affect the received message buffer
 | 
			
		||||
   * @returns Result indicating success or failure
 | 
			
		||||
   */
 | 
			
		||||
  public clearQueues(): Result<void, ChatManagerError> {
 | 
			
		||||
    try {
 | 
			
		||||
      this.messageQueue.clear();
 | 
			
		||||
      this.toastQueue.clear();
 | 
			
		||||
      return new Ok(undefined);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: `Failed to clear queues: ${String(error)}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Clears the received message buffer
 | 
			
		||||
   * @returns Result indicating success or failure
 | 
			
		||||
   */
 | 
			
		||||
  public clearBuffer(): Result<void, ChatManagerError> {
 | 
			
		||||
    try {
 | 
			
		||||
      this.chatBuffer.clear();
 | 
			
		||||
      return new Ok(undefined);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Err({
 | 
			
		||||
        kind: "ChatManager",
 | 
			
		||||
        reason: `Failed to clear buffer: ${String(error)}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tries to send a message immediately, bypassing the queue
 | 
			
		||||
   * @param message The message to send immediately
 | 
			
		||||
   * @returns Result indicating success or failure with error details
 | 
			
		||||
   */
 | 
			
		||||
  public sendMessageImmediate(message: ChatMessage): Result<void, ChatError> {
 | 
			
		||||
    return this.trySendMessage(message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tries to send a toast immediately, bypassing the queue
 | 
			
		||||
   * @param toast The toast to send immediately
 | 
			
		||||
   * @returns Result indicating success or failure with error details
 | 
			
		||||
   */
 | 
			
		||||
  public sendToastImmediate(toast: ChatToast): Result<void, ChatError> {
 | 
			
		||||
    return this.trySendToast(toast);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								src/lib/TimerManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/lib/TimerManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
import { pullEventAs, TimerEvent } from "./event";
 | 
			
		||||
import { Result, Ok, Err, Option, Some, None } from "./thirdparty/ts-result-es";
 | 
			
		||||
 | 
			
		||||
class TimerManager {
 | 
			
		||||
  private isRunning = false;
 | 
			
		||||
 | 
			
		||||
  private timerTaskMap = new Map<number, () => void>();
 | 
			
		||||
 | 
			
		||||
  // Don't put heavy logic on callback function
 | 
			
		||||
  public setTimeOut(delay: number, callback: () => void): void {
 | 
			
		||||
    const timerId = os.startTimer(delay);
 | 
			
		||||
    this.timerTaskMap.set(timerId, callback);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public run() {
 | 
			
		||||
    this.isRunning = true;
 | 
			
		||||
    while (this.isRunning) {
 | 
			
		||||
      const event = pullEventAs(TimerEvent, "timer");
 | 
			
		||||
      if (event === undefined) continue;
 | 
			
		||||
 | 
			
		||||
      const task = this.timerTaskMap.get(event.id);
 | 
			
		||||
      if (task === undefined) continue;
 | 
			
		||||
      task();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public stop() {
 | 
			
		||||
    this.isRunning = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public status(): boolean {
 | 
			
		||||
    return this.isRunning;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const gTimerManager = new TimerManager();
 | 
			
		||||
@@ -94,30 +94,36 @@ function processAndExecute<TContext extends object>(
 | 
			
		||||
): Result<void, CliError> {
 | 
			
		||||
  const { command, commandPath, options, remaining } = parseResult;
 | 
			
		||||
 | 
			
		||||
  // Handle requests for help on a specific command.
 | 
			
		||||
  if (shouldShowHelp([...remaining, ...Object.keys(options)])) {
 | 
			
		||||
    writer(generateHelp(command, commandPath));
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If a command has subcommands but no action, show its help page.
 | 
			
		||||
  if (
 | 
			
		||||
    command.subcommands &&
 | 
			
		||||
  // Unified Help Check:
 | 
			
		||||
  // A command should show its help page if:
 | 
			
		||||
  // 1. A help flag is explicitly passed (`--help` or `-h`). This has the highest priority.
 | 
			
		||||
  // 2. It's a command group that was called without a subcommand (i.e., it has no action).
 | 
			
		||||
  const isHelpFlagPassed = shouldShowHelp([
 | 
			
		||||
    ...remaining,
 | 
			
		||||
    ...Object.keys(options),
 | 
			
		||||
  ]);
 | 
			
		||||
  const isCommandGroupWithoutAction =
 | 
			
		||||
    command.subcommands !== undefined &&
 | 
			
		||||
    command.subcommands.size > 0 &&
 | 
			
		||||
    command.action === undefined
 | 
			
		||||
  ) {
 | 
			
		||||
    command.action === undefined;
 | 
			
		||||
 | 
			
		||||
  if (isHelpFlagPassed || isCommandGroupWithoutAction) {
 | 
			
		||||
    writer(generateHelp(command, commandPath));
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // A command that is meant to be executed must have an action.
 | 
			
		||||
  // If we are here, it's a runnable command. It must have an action.
 | 
			
		||||
  if (command.action === undefined) {
 | 
			
		||||
    // This case should ideally not be reached if the parser and the logic above are correct.
 | 
			
		||||
    // It would mean a command has no action and no subcommands, which is a configuration error.
 | 
			
		||||
    return new Err({
 | 
			
		||||
      kind: "NoAction",
 | 
			
		||||
      commandPath: [...commandPath, command.name],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Now we know it's a runnable command, and no help flag was passed.
 | 
			
		||||
  // We can now safely process the remaining items as arguments.
 | 
			
		||||
  return processArguments(command.args ?? [], remaining)
 | 
			
		||||
    .andThen((args) => {
 | 
			
		||||
      return processOptions(
 | 
			
		||||
 
 | 
			
		||||
@@ -103,5 +103,5 @@ export function generateCommandList<TContext extends object>(
 | 
			
		||||
 * @returns `true` if a help flag is found, otherwise `false`.
 | 
			
		||||
 */
 | 
			
		||||
export function shouldShowHelp(argv: string[]): boolean {
 | 
			
		||||
  return argv.includes("--help") || argv.includes("-h");
 | 
			
		||||
  return argv.includes("help") || argv.includes("h");
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -87,22 +87,49 @@ export function parseArguments<TContext extends object>(
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let currentCommand = rootCommand;
 | 
			
		||||
  let i = 0;
 | 
			
		||||
  let inOptions = false;
 | 
			
		||||
 | 
			
		||||
  const optionMapCache = new OptionMapCache();
 | 
			
		||||
  const getCurrentOptionMaps = () =>
 | 
			
		||||
    getOptionMaps(optionMapCache, currentCommand);
 | 
			
		||||
 | 
			
		||||
  while (i < argv.length) {
 | 
			
		||||
  // Cache option maps for current command - only updated when command changes
 | 
			
		||||
  let currentOptionMaps = getOptionMaps(optionMapCache, currentCommand);
 | 
			
		||||
 | 
			
		||||
  // Helper function to update command context and refresh option maps
 | 
			
		||||
  const updateCommand = (
 | 
			
		||||
    newCommand: Command<TContext>,
 | 
			
		||||
    commandName: string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    currentCommand = newCommand;
 | 
			
		||||
    result.command = currentCommand;
 | 
			
		||||
    result.commandPath.push(commandName);
 | 
			
		||||
    currentOptionMaps = getOptionMaps(optionMapCache, currentCommand);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Helper function to process option value
 | 
			
		||||
  const processOption = (optionName: string, i: number): number => {
 | 
			
		||||
    const optionDef = currentOptionMaps.optionMap.get(optionName);
 | 
			
		||||
    const nextArg = argv[i + 1];
 | 
			
		||||
    const isKnownBooleanOption =
 | 
			
		||||
      optionDef !== undefined && optionDef.defaultValue === undefined;
 | 
			
		||||
    const nextArgLooksLikeValue =
 | 
			
		||||
      nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
 | 
			
		||||
 | 
			
		||||
    if (nextArgLooksLikeValue && !isKnownBooleanOption) {
 | 
			
		||||
      result.options[optionName] = nextArg;
 | 
			
		||||
      return i + 1; // Skip the value argument
 | 
			
		||||
    } else {
 | 
			
		||||
      result.options[optionName] = true;
 | 
			
		||||
      return i;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Single pass through argv
 | 
			
		||||
  for (let i = 0; i < argv.length; i++) {
 | 
			
		||||
    const arg = argv[i];
 | 
			
		||||
 | 
			
		||||
    if (arg === undefined || arg === null) {
 | 
			
		||||
      i++;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    // Skip null/undefined arguments
 | 
			
		||||
    if (!arg) continue;
 | 
			
		||||
 | 
			
		||||
    // Handle double dash (--) - everything after is treated as a remaining argument
 | 
			
		||||
    // Handle double dash (--) - everything after is treated as remaining
 | 
			
		||||
    if (arg === "--") {
 | 
			
		||||
      result.remaining.push(...argv.slice(i + 1));
 | 
			
		||||
      break;
 | 
			
		||||
@@ -121,70 +148,33 @@ export function parseArguments<TContext extends object>(
 | 
			
		||||
      } else {
 | 
			
		||||
        // --option [value] format
 | 
			
		||||
        const optionName = arg.slice(2);
 | 
			
		||||
        const optionDef = getCurrentOptionMaps().optionMap.get(optionName);
 | 
			
		||||
 | 
			
		||||
        // Check if this is a known boolean option or if next arg looks like a value
 | 
			
		||||
        const nextArg = argv[i + 1];
 | 
			
		||||
        const isKnownBooleanOption =
 | 
			
		||||
          optionDef !== undefined && optionDef.defaultValue === undefined;
 | 
			
		||||
        const nextArgLooksLikeValue =
 | 
			
		||||
          nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
 | 
			
		||||
 | 
			
		||||
        if (nextArgLooksLikeValue && !isKnownBooleanOption) {
 | 
			
		||||
          result.options[optionName] = nextArg;
 | 
			
		||||
          i++; // Skip the value argument
 | 
			
		||||
        } else {
 | 
			
		||||
          // Boolean flag or no value available
 | 
			
		||||
          result.options[optionName] = true;
 | 
			
		||||
        }
 | 
			
		||||
        i = processOption(optionName, i);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle short options (-o or -o value)
 | 
			
		||||
    else if (arg.startsWith("-") && arg.length > 1) {
 | 
			
		||||
      inOptions = true;
 | 
			
		||||
      const shortName = arg.slice(1);
 | 
			
		||||
 | 
			
		||||
      // Get option maps for the new command (lazy loaded and cached)
 | 
			
		||||
      const maps = getCurrentOptionMaps();
 | 
			
		||||
      const optionName = maps.shortNameMap.get(shortName) ?? shortName;
 | 
			
		||||
      const optionDef = maps.optionMap.get(optionName);
 | 
			
		||||
 | 
			
		||||
      // Check if this is a known boolean option or if next arg looks like a value
 | 
			
		||||
      const nextArg = argv[i + 1];
 | 
			
		||||
      const isKnownBooleanOption =
 | 
			
		||||
        optionDef !== undefined && optionDef.defaultValue === undefined;
 | 
			
		||||
      const nextArgLooksLikeValue =
 | 
			
		||||
        nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
 | 
			
		||||
 | 
			
		||||
      if (nextArgLooksLikeValue && !isKnownBooleanOption) {
 | 
			
		||||
        result.options[optionName] = nextArg;
 | 
			
		||||
        i++; // Skip the value argument
 | 
			
		||||
      } else {
 | 
			
		||||
        // Boolean flag or no value available
 | 
			
		||||
        result.options[optionName] = true;
 | 
			
		||||
      }
 | 
			
		||||
      const optionName =
 | 
			
		||||
        currentOptionMaps.shortNameMap.get(shortName) ?? shortName;
 | 
			
		||||
      i = processOption(optionName, i);
 | 
			
		||||
    }
 | 
			
		||||
    // Handle positional arguments and command resolution
 | 
			
		||||
    else {
 | 
			
		||||
      if (!inOptions) {
 | 
			
		||||
        // Try to find this as a subcommand of the current command
 | 
			
		||||
        const subcommand = currentCommand.subcommands?.get(arg);
 | 
			
		||||
        if (subcommand !== undefined) {
 | 
			
		||||
          // Found a subcommand, move deeper
 | 
			
		||||
          currentCommand = subcommand;
 | 
			
		||||
          result.command = currentCommand;
 | 
			
		||||
          result.commandPath.push(arg);
 | 
			
		||||
        if (subcommand) {
 | 
			
		||||
          updateCommand(subcommand, arg);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Not a subcommand, treat as remaining argument
 | 
			
		||||
          result.remaining.push(arg);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // After options have started, treat as a remaining argument
 | 
			
		||||
        // After options have started, treat as remaining argument
 | 
			
		||||
        result.remaining.push(arg);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    i++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Ok(result);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								types/advanced-peripherals/shared.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								types/advanced-peripherals/shared.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -30,7 +30,7 @@ declare type MinecraftColor =
 | 
			
		||||
  | "light_purple"
 | 
			
		||||
  | "yellow"
 | 
			
		||||
  | "white"
 | 
			
		||||
  | "reset"; // RGB color in #RRGGBB format
 | 
			
		||||
  | `#${string}`;
 | 
			
		||||
 | 
			
		||||
declare type MinecraftFont =
 | 
			
		||||
  | "minecraft:default"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								types/craftos/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								types/craftos/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -925,10 +925,22 @@ declare namespace textutils {
 | 
			
		||||
  function pagedTabulate(...args: (LuaTable | Object | Color)[]): void;
 | 
			
		||||
  function serialize(tab: object, options?: SerializeOptions): string;
 | 
			
		||||
  function serialise(tab: object, options?: SerializeOptions): string;
 | 
			
		||||
  function serializeJSON(tab: object, nbtStyle?: boolean): string;
 | 
			
		||||
  function serializeJSON(tab: object, options: SerializeJSONOptions): string;
 | 
			
		||||
  function serialiseJSON(tab: object, nbtStyle?: boolean): string;
 | 
			
		||||
  function serialiseJSON(tab: object, options: SerializeJSONOptions): string;
 | 
			
		||||
  function serializeJSON(
 | 
			
		||||
    tab: object | string | number | boolean,
 | 
			
		||||
    nbtStyle?: boolean,
 | 
			
		||||
  ): string;
 | 
			
		||||
  function serializeJSON(
 | 
			
		||||
    tab: object | string | number | boolean,
 | 
			
		||||
    options: SerializeJSONOptions,
 | 
			
		||||
  ): string;
 | 
			
		||||
  function serialiseJSON(
 | 
			
		||||
    tab: object | string | number | boolean,
 | 
			
		||||
    nbtStyle?: boolean,
 | 
			
		||||
  ): string;
 | 
			
		||||
  function serialiseJSON(
 | 
			
		||||
    tab: object | string | number | boolean,
 | 
			
		||||
    options: SerializeJSONOptions,
 | 
			
		||||
  ): string;
 | 
			
		||||
  function unserialize(str: string): unknown;
 | 
			
		||||
  function unserialise(str: string): unknown;
 | 
			
		||||
  function unserializeJSON(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user