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 { CCLog } from "@/lib/ccLog";
|
||||||
import {
|
import {
|
||||||
AccessConfig,
|
AccessConfig,
|
||||||
@@ -5,574 +7,382 @@ import {
|
|||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import { ChatBoxEvent, pullEventAs } from "@/lib/event";
|
|
||||||
import { parseBoolean } from "@/lib/common";
|
import { parseBoolean } from "@/lib/common";
|
||||||
|
|
||||||
// CLI命令接口
|
// 1. Define AppContext
|
||||||
interface CLICommand {
|
export interface AppContext {
|
||||||
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 {
|
|
||||||
configFilepath: string;
|
configFilepath: string;
|
||||||
reloadConfig: () => void;
|
reloadConfig: () => void;
|
||||||
log: CCLog;
|
logger: CCLog;
|
||||||
chatBox: ChatBoxPeripheral;
|
print: (
|
||||||
|
message: string | MinecraftTextComponent | MinecraftTextComponent[],
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroupNames(config: AccessConfig) {
|
function getGroupNames(config: AccessConfig) {
|
||||||
return config.usersGroups.flatMap((value) => value.groupName);
|
return config.usersGroups.map((value) => value.groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 基础命令处理器
|
// 2. Define Commands
|
||||||
class CLICommandProcessor {
|
|
||||||
private commands = new Map<string, CLICommand>();
|
|
||||||
private context: CLIContext;
|
|
||||||
|
|
||||||
constructor(context: CLIContext) {
|
const addCommand: Command<AppContext> = {
|
||||||
this.context = context;
|
name: "add",
|
||||||
this.initializeCommands();
|
description: "添加玩家到用户组",
|
||||||
}
|
args: [
|
||||||
|
{
|
||||||
private initializeCommands() {
|
name: "userGroup",
|
||||||
// 注册所有命令
|
description: "要添加到的用户组",
|
||||||
this.registerCommand(new AddCommand());
|
required: true,
|
||||||
this.registerCommand(new DelCommand());
|
},
|
||||||
this.registerCommand(new ListCommand());
|
{ name: "playerName", description: "要添加的玩家", required: true },
|
||||||
this.registerCommand(new SetCommand());
|
],
|
||||||
this.registerCommand(new EditCommand());
|
action: ({ args, context }) => {
|
||||||
this.registerCommand(new ShowConfigCommand());
|
const [groupName, playerName] = [
|
||||||
this.registerCommand(new HelpCommand());
|
args.userGroup as string,
|
||||||
}
|
args.playerName as string,
|
||||||
|
];
|
||||||
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 config = loadConfig(context.configFilepath)!;
|
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) {
|
for (const groupConfig of config.usersGroups) {
|
||||||
const users = groupConfig.groupUsers ?? [];
|
const users = groupConfig.groupUsers ?? [];
|
||||||
message += `${groupConfig.groupName} : [ ${users.join(", ")} ]\n`;
|
message += `${groupConfig.groupName} : [ ${users.join(", ")} ]\n`;
|
||||||
}
|
}
|
||||||
|
context.print({ text: message.trim() });
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
const setCommand: Command<AppContext> = {
|
||||||
success: true,
|
name: "set",
|
||||||
message: message.trim(),
|
description: "配置访问控制设置",
|
||||||
};
|
args: [
|
||||||
}
|
{
|
||||||
}
|
name: "option",
|
||||||
|
description: "要设置的选项 (warnInterval, detectInterval, detectRange)",
|
||||||
// 设置命令
|
required: true,
|
||||||
class SetCommand implements CLICommand {
|
},
|
||||||
name = "set";
|
{ name: "value", description: "要设置的值", required: true },
|
||||||
description = "Config access control settings";
|
],
|
||||||
usage = "set <option> <value>";
|
action: ({ args, context }) => {
|
||||||
|
const [option, valueStr] = [args.option as string, args.value as string];
|
||||||
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 value = parseInt(valueStr);
|
const value = parseInt(valueStr);
|
||||||
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
return {
|
context.print({ text: `无效的值: ${valueStr}. 必须是一个数字。` });
|
||||||
success: false,
|
return Ok.EMPTY;
|
||||||
message: `Invalid value: ${valueStr}. Must be a number.`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: AccessConfig = loadConfig(context.configFilepath)!;
|
const config = loadConfig(context.configFilepath)!;
|
||||||
|
let message = "";
|
||||||
|
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case "warnInterval":
|
case "warnInterval":
|
||||||
config.watchInterval = value;
|
config.watchInterval = value;
|
||||||
return {
|
message = `已设置警告间隔为 ${value}`;
|
||||||
success: true,
|
break;
|
||||||
message: `Set warn interval to ${config.watchInterval}`,
|
|
||||||
shouldSaveConfig: true,
|
|
||||||
config,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "detectInterval":
|
case "detectInterval":
|
||||||
config.detectInterval = value;
|
config.detectInterval = value;
|
||||||
return {
|
message = `已设置检测间隔为 ${value}`;
|
||||||
success: true,
|
break;
|
||||||
message: `Set detect interval to ${config.detectInterval}`,
|
|
||||||
shouldSaveConfig: true,
|
|
||||||
config,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "detectRange":
|
case "detectRange":
|
||||||
config.detectRange = value;
|
config.detectRange = value;
|
||||||
return {
|
message = `已设置检测范围为 ${value}`;
|
||||||
success: true,
|
break;
|
||||||
message: `Set detect range to ${config.detectRange}`,
|
|
||||||
shouldSaveConfig: true,
|
|
||||||
config,
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {
|
context.print({
|
||||||
success: false,
|
text: `未知选项: ${option}. 可用选项: warnInterval, detectInterval, detectRange`,
|
||||||
message: `Unknown option: ${option}. Available options: warnInterval, detectInterval, detectRange`,
|
});
|
||||||
};
|
return Ok.EMPTY;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 帮助命令
|
saveConfig(config, context.configFilepath);
|
||||||
class HelpCommand implements CLICommand {
|
context.reloadConfig();
|
||||||
name = "help";
|
context.print({ text: message });
|
||||||
description = "Show command help";
|
return Ok.EMPTY;
|
||||||
usage = "help";
|
},
|
||||||
|
};
|
||||||
|
|
||||||
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 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;
|
let groupConfig: UserGroupConfig | undefined;
|
||||||
|
|
||||||
if (groupName === "admin") {
|
if (groupName === "admin") {
|
||||||
groupConfig = config.adminGroupConfig;
|
groupConfig = config.adminGroupConfig;
|
||||||
} else {
|
} else {
|
||||||
groupConfig = config.usersGroups.find(
|
groupConfig = config.usersGroups.find((g) => g.groupName === groupName);
|
||||||
(group) => group.groupName === groupName,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupConfig) {
|
if (!groupConfig) {
|
||||||
return {
|
context.print({ text: `用户组 ${groupName} 未找到` });
|
||||||
success: false,
|
return Ok.EMPTY;
|
||||||
message: `Group ${groupName} not found`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const boolValue = parseBoolean(valueStr);
|
||||||
|
if (boolValue === undefined) {
|
||||||
|
context.print({
|
||||||
|
text: `无效的布尔值: ${valueStr}. 请使用 'true' 或 'false'.`,
|
||||||
|
});
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = "";
|
||||||
switch (property) {
|
switch (property) {
|
||||||
case "isAllowed": {
|
case "isAllowed":
|
||||||
const val = parseBoolean(valueStr);
|
groupConfig.isAllowed = boolValue;
|
||||||
if (val != undefined) {
|
message = `已设置 ${groupName}.isAllowed 为 ${boolValue}`;
|
||||||
groupConfig.isAllowed = val;
|
break;
|
||||||
return {
|
case "isNotice":
|
||||||
success: true,
|
groupConfig.isNotice = boolValue;
|
||||||
message: `Set ${groupName}.isAllowed to ${groupConfig.isAllowed}`,
|
message = `已设置 ${groupName}.isNotice 为 ${boolValue}`;
|
||||||
shouldSaveConfig: true,
|
break;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {
|
context.print({
|
||||||
success: false,
|
text: `未知属性: ${property}. 可用属性: isAllowed, isNotice`,
|
||||||
message: `Unknown property: ${property}. Available: isAllowed, isNotice`,
|
});
|
||||||
};
|
return Ok.EMPTY;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示配置命令
|
saveConfig(config, context.configFilepath);
|
||||||
class ShowConfigCommand implements CLICommand {
|
context.reloadConfig();
|
||||||
name = "showconfig";
|
context.print({ text: message });
|
||||||
description = "Show configuration";
|
return Ok.EMPTY;
|
||||||
usage = "showconfig [type]";
|
},
|
||||||
|
};
|
||||||
|
|
||||||
execute(args: string[], _executor: string, context: CLIContext): CLIResult {
|
const editCommand: Command<AppContext> = {
|
||||||
const type = args[0] || "all";
|
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)!;
|
const config = loadConfig(context.configFilepath)!;
|
||||||
|
let message = "";
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "groups": {
|
case "groups": {
|
||||||
let groupsMessage = `Admin Group: ${config.adminGroupConfig.groupName}\n`;
|
let groupsMessage = `管理员组: ${config.adminGroupConfig.groupName}\n`;
|
||||||
groupsMessage += ` Users: [${config.adminGroupConfig.groupUsers.join(", ")}]\n`;
|
groupsMessage += ` 用户: [${config.adminGroupConfig.groupUsers.join(
|
||||||
groupsMessage += ` Allowed: ${config.adminGroupConfig.isAllowed}\n`;
|
", ",
|
||||||
groupsMessage += ` notice: ${config.adminGroupConfig.isNotice}\n\n`;
|
)}]\n`;
|
||||||
|
groupsMessage += ` 允许: ${config.adminGroupConfig.isAllowed}\n`;
|
||||||
|
groupsMessage += ` 通知: ${config.adminGroupConfig.isNotice}\n\n`;
|
||||||
|
|
||||||
for (const group of config.usersGroups) {
|
for (const group of config.usersGroups) {
|
||||||
groupsMessage += `Group: ${group.groupName}\n`;
|
groupsMessage += `用户组: ${group.groupName}\n`;
|
||||||
groupsMessage += ` Users: [${(group.groupUsers ?? []).join(", ")}]\n`;
|
groupsMessage += ` 用户: [${(group.groupUsers ?? []).join(", ")}]\n`;
|
||||||
groupsMessage += ` Allowed: ${group.isAllowed}\n`;
|
groupsMessage += ` 允许: ${group.isAllowed}\n`;
|
||||||
groupsMessage += ` Notice: ${group.isNotice}\n`;
|
groupsMessage += ` 通知: ${group.isNotice}\n`;
|
||||||
groupsMessage += "\n";
|
groupsMessage += "\n";
|
||||||
}
|
}
|
||||||
|
message = groupsMessage.trim();
|
||||||
return {
|
break;
|
||||||
success: true,
|
|
||||||
message: groupsMessage.trim(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "toast": {
|
case "toast": {
|
||||||
let toastMessage = "Default Toast Config:\n";
|
let toastMessage = "默认 Toast 配置:\n";
|
||||||
toastMessage += ` Title: ${config.welcomeToastConfig.title.text}\n`;
|
toastMessage += ` 标题: ${config.welcomeToastConfig.title.text}\n`;
|
||||||
toastMessage += ` Message: ${config.welcomeToastConfig.msg.text}\n`;
|
toastMessage += ` 消息: ${config.welcomeToastConfig.msg.text}\n`;
|
||||||
toastMessage += ` Prefix: ${config.welcomeToastConfig.prefix ?? "none"}\n`;
|
toastMessage += ` 前缀: ${
|
||||||
toastMessage += ` Brackets: ${config.welcomeToastConfig.brackets ?? "none"}\n`;
|
config.welcomeToastConfig.prefix ?? "none"
|
||||||
toastMessage += ` Bracket Color: ${config.welcomeToastConfig.bracketColor ?? "none"}\n\n`;
|
}\n`;
|
||||||
|
toastMessage += ` 括号: ${
|
||||||
|
config.welcomeToastConfig.brackets ?? "none"
|
||||||
|
}\n`;
|
||||||
|
toastMessage += ` 括号颜色: ${
|
||||||
|
config.welcomeToastConfig.bracketColor ?? "none"
|
||||||
|
}\n\n`;
|
||||||
|
|
||||||
toastMessage += "Warn Toast Config:\n";
|
toastMessage += "警告 Toast 配置:\n";
|
||||||
toastMessage += ` Title: ${config.warnToastConfig.title.text}\n`;
|
toastMessage += ` 标题: ${config.warnToastConfig.title.text}\n`;
|
||||||
toastMessage += ` Message: ${config.warnToastConfig.msg.text}\n`;
|
toastMessage += ` 消息: ${config.warnToastConfig.msg.text}\n`;
|
||||||
toastMessage += ` Prefix: ${config.warnToastConfig.prefix ?? "none"}\n`;
|
toastMessage += ` 前缀: ${config.warnToastConfig.prefix ?? "none"}\n`;
|
||||||
toastMessage += ` Brackets: ${config.warnToastConfig.brackets ?? "none"}\n`;
|
toastMessage += ` 括号: ${
|
||||||
toastMessage += ` Bracket Color: ${config.warnToastConfig.bracketColor ?? "none"}`;
|
config.warnToastConfig.brackets ?? "none"
|
||||||
|
}\n`;
|
||||||
return {
|
toastMessage += ` 括号颜色: ${
|
||||||
success: true,
|
config.warnToastConfig.bracketColor ?? "none"
|
||||||
message: toastMessage,
|
}`;
|
||||||
};
|
message = toastMessage;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "all": {
|
case "all": {
|
||||||
let allMessage = `Detect Range: ${config.detectRange}\n`;
|
let allMessage = `检测范围: ${config.detectRange}\n`;
|
||||||
allMessage += `Detect Interval: ${config.detectInterval}\n`;
|
allMessage += `检测间隔: ${config.detectInterval}\n`;
|
||||||
allMessage += `Warn Interval: ${config.watchInterval}\n\n`;
|
allMessage += `警告间隔: ${config.watchInterval}\n\n`;
|
||||||
allMessage +=
|
allMessage +=
|
||||||
"Use 'showconfig groups' or 'showconfig toast' for detailed view";
|
"使用 'showconfig --type groups' 或 'showconfig --type toast' 查看详细信息";
|
||||||
|
message = allMessage;
|
||||||
return {
|
break;
|
||||||
success: true,
|
|
||||||
message: allMessage,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {
|
message = `无效类型: ${type}. 可用类型: groups, toast, all`;
|
||||||
success: false,
|
break;
|
||||||
message: `Invalid type: ${type}. Available: groups, toast, all`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
context.print({ text: message });
|
||||||
|
return Ok.EMPTY;
|
||||||
// CLI循环处理器
|
},
|
||||||
export class AccessControlCLI {
|
};
|
||||||
private processor: CLICommandProcessor;
|
|
||||||
private context: CLIContext;
|
// Root command
|
||||||
|
const rootCommand: Command<AppContext> = {
|
||||||
constructor(context: CLIContext) {
|
name: "@AC",
|
||||||
this.context = context;
|
description: "访问控制命令行界面",
|
||||||
this.processor = new CLICommandProcessor(context);
|
subcommands: new Map([
|
||||||
}
|
["add", addCommand],
|
||||||
|
["del", delCommand],
|
||||||
public startConfigLoop() {
|
["list", listCommand],
|
||||||
while (true) {
|
["set", setCommand],
|
||||||
const ev = pullEventAs(ChatBoxEvent, "chat");
|
["edit", editCommand],
|
||||||
|
["showconfig", showConfigCommand],
|
||||||
if (ev === undefined) continue;
|
]),
|
||||||
|
action: ({ context }) => {
|
||||||
const config = loadConfig(this.context.configFilepath)!;
|
context.print([
|
||||||
if (!config.adminGroupConfig.groupUsers.includes(ev.username)) continue;
|
{
|
||||||
if (!ev.message.startsWith("@AC")) continue;
|
text: "请使用 ",
|
||||||
|
},
|
||||||
this.context.log.info(
|
{
|
||||||
`Received command "${ev.message}" from admin ${ev.username}`,
|
text: "@AC --help",
|
||||||
);
|
clickEvent: {
|
||||||
|
action: "copy_to_clipboard",
|
||||||
const result = this.processor.processCommand(ev.message, ev.username);
|
value: "@AC --help",
|
||||||
this.processor.sendResponse(result, ev.username);
|
},
|
||||||
if (!result.success) {
|
hoverEvent: {
|
||||||
this.context.log.warn(`Command failed: ${result.message}`);
|
action: "show_text",
|
||||||
}
|
value: "点击复制命令",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
|
text: " 获取门禁系统更详细的命令说明😊😊😊",
|
||||||
// 导出类型和工厂函数
|
},
|
||||||
export { CLIContext, CLIResult, CLICommand };
|
]);
|
||||||
|
return Ok.EMPTY;
|
||||||
export function createAccessControlCLI(context: CLIContext): AccessControlCLI {
|
},
|
||||||
return new AccessControlCLI(context);
|
};
|
||||||
|
|
||||||
|
export function createAccessControlCli(context: AppContext) {
|
||||||
|
return createCli(rootCommand, {
|
||||||
|
globalContext: context,
|
||||||
|
writer: (msg) => context.print(msg),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface UserGroupConfig {
|
|||||||
groupName: string;
|
groupName: string;
|
||||||
isAllowed: boolean;
|
isAllowed: boolean;
|
||||||
isNotice: boolean;
|
isNotice: boolean;
|
||||||
|
isWelcome: boolean;
|
||||||
groupUsers: string[];
|
groupUsers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ const defaultConfig: AccessConfig = {
|
|||||||
groupUsers: ["Selcon"],
|
groupUsers: ["Selcon"],
|
||||||
isAllowed: true,
|
isAllowed: true,
|
||||||
isNotice: true,
|
isNotice: true,
|
||||||
|
isWelcome: true,
|
||||||
},
|
},
|
||||||
usersGroups: [
|
usersGroups: [
|
||||||
{
|
{
|
||||||
@@ -46,57 +48,60 @@ const defaultConfig: AccessConfig = {
|
|||||||
groupUsers: [],
|
groupUsers: [],
|
||||||
isAllowed: true,
|
isAllowed: true,
|
||||||
isNotice: true,
|
isNotice: true,
|
||||||
|
isWelcome: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
groupName: "VIP",
|
groupName: "VIP",
|
||||||
groupUsers: [],
|
groupUsers: [],
|
||||||
isAllowed: true,
|
isAllowed: true,
|
||||||
isNotice: false,
|
isNotice: false,
|
||||||
|
isWelcome: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
groupName: "enemies",
|
groupName: "enemies",
|
||||||
groupUsers: [],
|
groupUsers: [],
|
||||||
isAllowed: false,
|
isAllowed: false,
|
||||||
isNotice: false,
|
isNotice: false,
|
||||||
|
isWelcome: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
welcomeToastConfig: {
|
welcomeToastConfig: {
|
||||||
title: {
|
title: {
|
||||||
text: "Welcome",
|
text: "欢迎",
|
||||||
color: "green",
|
color: "green",
|
||||||
},
|
},
|
||||||
msg: {
|
msg: {
|
||||||
text: "Hello User %playerName%",
|
text: "欢迎 %playerName% 参观桃源星喵~",
|
||||||
color: "green",
|
color: "#EDC8DA",
|
||||||
},
|
},
|
||||||
prefix: "Taohuayuan",
|
prefix: "桃源星",
|
||||||
brackets: "[]",
|
brackets: "<>",
|
||||||
bracketColor: "",
|
bracketColor: "",
|
||||||
},
|
},
|
||||||
noticeToastConfig: {
|
noticeToastConfig: {
|
||||||
title: {
|
title: {
|
||||||
text: "Notice",
|
text: "警告",
|
||||||
color: "red",
|
color: "red",
|
||||||
},
|
},
|
||||||
msg: {
|
msg: {
|
||||||
text: "Unfamiliar player %playerName% appeared at Position %playerPosX%, %playerPosY%, %playerPosZ%",
|
text: "陌生玩家 %playerName% 出现在 %playerPosX%, %playerPosY%, %playerPosZ%",
|
||||||
color: "red",
|
color: "red",
|
||||||
},
|
},
|
||||||
prefix: "Taohuayuan",
|
prefix: "桃源星",
|
||||||
brackets: "[]",
|
brackets: "<>",
|
||||||
bracketColor: "",
|
bracketColor: "",
|
||||||
},
|
},
|
||||||
warnToastConfig: {
|
warnToastConfig: {
|
||||||
title: {
|
title: {
|
||||||
text: "Attention!!!",
|
text: "注意",
|
||||||
color: "red",
|
color: "red",
|
||||||
},
|
},
|
||||||
msg: {
|
msg: {
|
||||||
text: "%playerName% you are not allowed to be here",
|
text: "%playerName% 你已经进入桃源星领地",
|
||||||
color: "red",
|
color: "red",
|
||||||
},
|
},
|
||||||
prefix: "Taohuayuan",
|
prefix: "桃源星",
|
||||||
brackets: "[]",
|
brackets: "<>",
|
||||||
bracketColor: "",
|
bracketColor: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { CCLog, DAY, LogLevel } from "@/lib/ccLog";
|
import { CCLog, DAY, LogLevel } from "@/lib/ccLog";
|
||||||
import { ToastConfig, UserGroupConfig, loadConfig } from "./config";
|
import { ToastConfig, UserGroupConfig, loadConfig } from "./config";
|
||||||
import { createAccessControlCLI } from "./cli";
|
import { createAccessControlCli } from "./cli";
|
||||||
import { launchAccessControlTUI } from "./tui";
|
import { launchAccessControlTUI } from "./tui";
|
||||||
import * as peripheralManager from "../lib/PeripheralManager";
|
import * as peripheralManager from "../lib/PeripheralManager";
|
||||||
import { deepCopy } from "@/lib/common";
|
import { deepCopy } from "@/lib/common";
|
||||||
import { ReadWriteLock } from "@/lib/mutex/ReadWriteLock";
|
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];
|
const args = [...$vararg];
|
||||||
|
|
||||||
@@ -12,7 +15,7 @@ const args = [...$vararg];
|
|||||||
const logger = new CCLog("accesscontrol.log", {
|
const logger = new CCLog("accesscontrol.log", {
|
||||||
printTerminal: true,
|
printTerminal: true,
|
||||||
logInterval: DAY,
|
logInterval: DAY,
|
||||||
outputMinLevel: LogLevel.Info,
|
outputMinLevel: LogLevel.Debug,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load Config
|
// Load Config
|
||||||
@@ -23,16 +26,20 @@ logger.info("Load config successfully!");
|
|||||||
logger.debug(textutils.serialise(config, { allow_repetitions: true }));
|
logger.debug(textutils.serialise(config, { allow_repetitions: true }));
|
||||||
|
|
||||||
// Peripheral
|
// Peripheral
|
||||||
const playerDetector = peripheralManager.findByNameRequired("playerDetector");
|
const playerDetector = peripheral.find(
|
||||||
const chatBox = peripheralManager.findByNameRequired("chatBox");
|
"playerDetector",
|
||||||
|
)[0] as PlayerDetectorPeripheral;
|
||||||
|
const chatBox = peripheral.find("chatBox")[0] as ChatBoxPeripheral;
|
||||||
|
const chatManager: ChatManager = new ChatManager([chatBox]);
|
||||||
|
|
||||||
// Global
|
// Global
|
||||||
let inRangePlayers: string[] = [];
|
let gInRangePlayers: string[] = [];
|
||||||
let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
|
let gWatchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
|
||||||
|
let gIsRunning = true;
|
||||||
|
|
||||||
interface ParseParams {
|
interface ParseParams {
|
||||||
name?: string;
|
playerName?: string;
|
||||||
group?: string;
|
groupName?: string;
|
||||||
info?: PlayerInfo;
|
info?: PlayerInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +51,8 @@ function reloadConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config = loadConfig(configFilepath)!;
|
config = loadConfig(configFilepath)!;
|
||||||
inRangePlayers = [];
|
gInRangePlayers = [];
|
||||||
watchPlayersInfo = [];
|
gWatchPlayersInfo = [];
|
||||||
releaser.release();
|
releaser.release();
|
||||||
logger.info("Reload config successfully!");
|
logger.info("Reload config successfully!");
|
||||||
}
|
}
|
||||||
@@ -53,7 +60,7 @@ function reloadConfig() {
|
|||||||
function safeParseTextComponent(
|
function safeParseTextComponent(
|
||||||
component: MinecraftTextComponent,
|
component: MinecraftTextComponent,
|
||||||
params?: ParseParams,
|
params?: ParseParams,
|
||||||
): string {
|
): MinecraftTextComponent {
|
||||||
const newComponent = deepCopy(component);
|
const newComponent = deepCopy(component);
|
||||||
|
|
||||||
if (newComponent.text == undefined) {
|
if (newComponent.text == undefined) {
|
||||||
@@ -61,11 +68,11 @@ function safeParseTextComponent(
|
|||||||
} else if (newComponent.text.includes("%")) {
|
} else if (newComponent.text.includes("%")) {
|
||||||
newComponent.text = newComponent.text.replace(
|
newComponent.text = newComponent.text.replace(
|
||||||
"%playerName%",
|
"%playerName%",
|
||||||
params?.name ?? "UnknowPlayer",
|
params?.playerName ?? "UnknowPlayer",
|
||||||
);
|
);
|
||||||
newComponent.text = newComponent.text.replace(
|
newComponent.text = newComponent.text.replace(
|
||||||
"%groupName%",
|
"%groupName%",
|
||||||
params?.group ?? "UnknowGroup",
|
params?.groupName ?? "UnknowGroup",
|
||||||
);
|
);
|
||||||
newComponent.text = newComponent.text.replace(
|
newComponent.text = newComponent.text.replace(
|
||||||
"%playerPosX%",
|
"%playerPosX%",
|
||||||
@@ -80,7 +87,34 @@ function safeParseTextComponent(
|
|||||||
params?.info?.z.toString() ?? "UnknowPosZ",
|
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(
|
function sendToast(
|
||||||
@@ -94,22 +128,22 @@ function sendToast(
|
|||||||
releaser = configLock.tryAcquireRead();
|
releaser = configLock.tryAcquireRead();
|
||||||
}
|
}
|
||||||
|
|
||||||
chatBox.sendFormattedToastToPlayer(
|
chatManager.sendToast({
|
||||||
safeParseTextComponent(
|
message: safeParseTextComponent(
|
||||||
toastConfig.msg ?? config.welcomeToastConfig.msg,
|
toastConfig.msg ?? config.welcomeToastConfig.msg,
|
||||||
params,
|
params,
|
||||||
),
|
),
|
||||||
safeParseTextComponent(
|
title: safeParseTextComponent(
|
||||||
toastConfig.title ?? config.welcomeToastConfig.title,
|
toastConfig.title ?? config.welcomeToastConfig.title,
|
||||||
params,
|
params,
|
||||||
),
|
),
|
||||||
targetPlayer,
|
prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
||||||
toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
|
||||||
toastConfig.brackets ?? config.welcomeToastConfig.brackets,
|
bracketColor:
|
||||||
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
|
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
|
||||||
undefined,
|
targetPlayer: targetPlayer,
|
||||||
true,
|
utf8Support: true,
|
||||||
);
|
});
|
||||||
releaser.release();
|
releaser.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +165,7 @@ function sendNotice(player: string, playerInfo?: PlayerInfo) {
|
|||||||
for (const targetPlayer of noticeTargetPlayers) {
|
for (const targetPlayer of noticeTargetPlayers) {
|
||||||
if (!onlinePlayers.includes(targetPlayer)) continue;
|
if (!onlinePlayers.includes(targetPlayer)) continue;
|
||||||
sendToast(config.noticeToastConfig, targetPlayer, {
|
sendToast(config.noticeToastConfig, targetPlayer, {
|
||||||
name: player,
|
playerName: player,
|
||||||
info: playerInfo,
|
info: playerInfo,
|
||||||
});
|
});
|
||||||
sleep(1);
|
sleep(1);
|
||||||
@@ -149,32 +183,32 @@ function sendWarn(player: string) {
|
|||||||
releaser = configLock.tryAcquireRead();
|
releaser = configLock.tryAcquireRead();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToast(config.warnToastConfig, player, { name: player });
|
sendToast(config.warnToastConfig, player, { playerName: player });
|
||||||
chatBox.sendFormattedMessageToPlayer(
|
chatManager.sendMessage({
|
||||||
safeParseTextComponent(config.warnToastConfig.msg, { name: player }),
|
message: safeParseTextComponent(config.warnToastConfig.msg, {
|
||||||
player,
|
playerName: player,
|
||||||
"AccessControl",
|
}),
|
||||||
"[]",
|
targetPlayer: player,
|
||||||
undefined,
|
prefix: "AccessControl",
|
||||||
undefined,
|
brackets: "[]",
|
||||||
true,
|
utf8Support: true,
|
||||||
);
|
});
|
||||||
releaser.release();
|
releaser.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchLoop() {
|
function watchLoop() {
|
||||||
while (true) {
|
while (gIsRunning) {
|
||||||
const releaser = configLock.tryAcquireRead();
|
const releaser = configLock.tryAcquireRead();
|
||||||
if (releaser === undefined) {
|
if (releaser === undefined) {
|
||||||
os.sleep(1);
|
os.sleep(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchPlayerNames = watchPlayersInfo.flatMap((value) => value.name);
|
const watchPlayerNames = gWatchPlayersInfo.flatMap((value) => value.name);
|
||||||
logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
|
logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
|
||||||
for (const player of watchPlayersInfo) {
|
for (const player of gWatchPlayersInfo) {
|
||||||
const playerInfo = playerDetector.getPlayerPos(player.name);
|
const playerInfo = playerDetector.getPlayerPos(player.name);
|
||||||
if (inRangePlayers.includes(player.name)) {
|
if (gInRangePlayers.includes(player.name)) {
|
||||||
// Notice
|
// Notice
|
||||||
if (player.hasNoticeTimes < config.noticeTimes) {
|
if (player.hasNoticeTimes < config.noticeTimes) {
|
||||||
sendNotice(player.name, playerInfo);
|
sendNotice(player.name, playerInfo);
|
||||||
@@ -190,7 +224,7 @@ function watchLoop() {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Get rid of player from list
|
// Get rid of player from list
|
||||||
watchPlayersInfo = watchPlayersInfo.filter(
|
gWatchPlayersInfo = gWatchPlayersInfo.filter(
|
||||||
(value) => value.name != player.name,
|
(value) => value.name != player.name,
|
||||||
);
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -206,7 +240,7 @@ function watchLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mainLoop() {
|
function mainLoop() {
|
||||||
while (true) {
|
while (gIsRunning) {
|
||||||
const releaser = configLock.tryAcquireRead();
|
const releaser = configLock.tryAcquireRead();
|
||||||
if (releaser === undefined) {
|
if (releaser === undefined) {
|
||||||
os.sleep(0.1);
|
os.sleep(0.1);
|
||||||
@@ -218,20 +252,31 @@ function mainLoop() {
|
|||||||
logger.debug(`Detected ${players.length} players: ${playersList}`);
|
logger.debug(`Detected ${players.length} players: ${playersList}`);
|
||||||
|
|
||||||
for (const player of players) {
|
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)) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New player appear
|
// New player appear
|
||||||
const playerInfo = playerDetector.getPlayerPos(player);
|
|
||||||
let groupConfig: UserGroupConfig = {
|
let groupConfig: UserGroupConfig = {
|
||||||
groupName: "Unfamiliar",
|
groupName: "Unfamiliar",
|
||||||
groupUsers: [],
|
groupUsers: [],
|
||||||
isAllowed: false,
|
isAllowed: false,
|
||||||
isNotice: false,
|
isNotice: false,
|
||||||
|
isWelcome: false,
|
||||||
};
|
};
|
||||||
for (const userGroupConfig of config.usersGroups) {
|
for (const userGroupConfig of config.usersGroups) {
|
||||||
if (userGroupConfig.groupUsers == undefined) continue;
|
if (userGroupConfig.groupUsers == undefined) continue;
|
||||||
@@ -244,28 +289,37 @@ function mainLoop() {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.adminGroupConfig.isWelcome)
|
||||||
|
sendMessage(config.welcomeToastConfig, player, {
|
||||||
|
playerName: player,
|
||||||
|
groupName: groupConfig.groupName,
|
||||||
|
info: playerInfo,
|
||||||
|
});
|
||||||
if (groupConfig.isAllowed) continue;
|
if (groupConfig.isAllowed) continue;
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||||
);
|
);
|
||||||
if (config.isWarn) sendWarn(player);
|
if (config.isWarn) sendWarn(player);
|
||||||
watchPlayersInfo = [
|
gWatchPlayersInfo = [
|
||||||
...watchPlayersInfo,
|
...gWatchPlayersInfo,
|
||||||
{ name: player, hasNoticeTimes: 0 },
|
{ name: player, hasNoticeTimes: 0 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
inRangePlayers = players;
|
gInRangePlayers = players;
|
||||||
releaser.release();
|
releaser.release();
|
||||||
os.sleep(config.detectInterval);
|
os.sleep(config.detectInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyboardLoop() {
|
function keyboardLoop() {
|
||||||
while (true) {
|
while (gIsRunning) {
|
||||||
const [eventType, key] = os.pullEvent("key");
|
const event = pullEventAs(KeyEvent, "key");
|
||||||
if (eventType === "key" && key === keys.c) {
|
if (event === undefined) continue;
|
||||||
|
|
||||||
|
if (event.key === keys.c) {
|
||||||
logger.info("Launching Access Control TUI...");
|
logger.info("Launching Access Control TUI...");
|
||||||
try {
|
try {
|
||||||
logger.setInTerminal(false);
|
logger.setInTerminal(false);
|
||||||
@@ -277,7 +331,66 @@ function keyboardLoop() {
|
|||||||
logger.setInTerminal(true);
|
logger.setInTerminal(true);
|
||||||
reloadConfig();
|
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(", "));
|
logger.info("Starting access control system, get args: " + args.join(", "));
|
||||||
if (args.length == 1) {
|
if (args.length == 1) {
|
||||||
if (args[0] == "start") {
|
if (args[0] == "start") {
|
||||||
// 创建CLI处理器
|
const tutorial: string[] = [];
|
||||||
const cli = createAccessControlCLI({
|
tutorial.push("Access Control System started.");
|
||||||
configFilepath: configFilepath,
|
tutorial.push("\tPress 'c' to open configuration TUI.");
|
||||||
reloadConfig: () => reloadConfig(),
|
tutorial.push("\tPress 'r' to reload configuration.");
|
||||||
log: logger,
|
print(tutorial.join("\n"));
|
||||||
chatBox: chatBox,
|
|
||||||
});
|
|
||||||
|
|
||||||
print(
|
|
||||||
"Access Control System started. Press 'c' to open configuration TUI.",
|
|
||||||
);
|
|
||||||
parallel.waitForAll(
|
parallel.waitForAll(
|
||||||
() => {
|
() => mainLoop(),
|
||||||
mainLoop();
|
() => gTimerManager.run(),
|
||||||
},
|
() => cliLoop(),
|
||||||
() => {
|
() => watchLoop(),
|
||||||
cli.startConfigLoop();
|
() => keyboardLoop(),
|
||||||
},
|
() => chatManager.run(),
|
||||||
() => {
|
|
||||||
watchLoop();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
keyboardLoop();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -355,6 +355,34 @@ const AccessControlTUI = () => {
|
|||||||
{ class: "flex flex-col ml-2" },
|
{ class: "flex flex-col ml-2" },
|
||||||
label({}, () => `Group: ${getSelectedGroup().groupName}`),
|
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(
|
div(
|
||||||
{ class: "flex flex-row" },
|
{ class: "flex flex-row" },
|
||||||
label({}, "Is Allowed:"),
|
label({}, "Is Allowed:"),
|
||||||
@@ -541,7 +569,12 @@ const AccessControlTUI = () => {
|
|||||||
label({}, "Prefix:"),
|
label({}, "Prefix:"),
|
||||||
input({
|
input({
|
||||||
type: "text",
|
type: "text",
|
||||||
value: () => getTempToastConfig().prefix,
|
value: () => {
|
||||||
|
const str = textutils.serialiseJSON(getTempToastConfig().prefix, {
|
||||||
|
unicode_strings: true,
|
||||||
|
});
|
||||||
|
return str.substring(1, str.length - 1);
|
||||||
|
},
|
||||||
onInput: (value) =>
|
onInput: (value) =>
|
||||||
setTempToastConfig({ ...getTempToastConfig(), prefix: value }),
|
setTempToastConfig({ ...getTempToastConfig(), prefix: value }),
|
||||||
onFocusChanged: () => {
|
onFocusChanged: () => {
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* Example CLI application demonstrating the ccCLI framework
|
* Example CLI application demonstrating the ccCLI framework
|
||||||
* This example shows how to create a calculator CLI with global context injection
|
* 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 { Command, createCli, CliError } from "../lib/ccCLI/index";
|
||||||
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
|
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
|
||||||
|
import { ChatManager, ChatMessage, ChatToast } from "../lib/ChatManager";
|
||||||
|
|
||||||
// 1. Define global context type
|
// 1. Define global context type
|
||||||
interface AppContext {
|
interface AppContext {
|
||||||
appName: string;
|
appName: string;
|
||||||
log: (message: string) => void;
|
log: (message: string) => void;
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
|
chatManager?: ChatManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Define individual commands
|
// 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
|
// 3. Define root command
|
||||||
const rootCommand: Command<AppContext> = {
|
const rootCommand: Command<AppContext> = {
|
||||||
name: "calculator",
|
name: "calculator",
|
||||||
description: "A feature-rich calculator program",
|
description: "A feature-rich calculator and chat management program",
|
||||||
options: new Map([
|
options: new Map([
|
||||||
[
|
[
|
||||||
"debug",
|
"debug",
|
||||||
@@ -172,6 +511,7 @@ const rootCommand: Command<AppContext> = {
|
|||||||
["math", mathCommand],
|
["math", mathCommand],
|
||||||
["greet", greetCommand],
|
["greet", greetCommand],
|
||||||
["config", configCommand],
|
["config", configCommand],
|
||||||
|
["chat", chatCommand],
|
||||||
]),
|
]),
|
||||||
action: ({ options, context }): Result<void, CliError> => {
|
action: ({ options, context }): Result<void, CliError> => {
|
||||||
// Update debug mode from command line option
|
// Update debug mode from command line option
|
||||||
@@ -183,20 +523,58 @@ const rootCommand: Command<AppContext> = {
|
|||||||
|
|
||||||
print(`Welcome to ${context.appName}!`);
|
print(`Welcome to ${context.appName}!`);
|
||||||
print("Use --help to see available commands");
|
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;
|
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 = {
|
const appContext: AppContext = {
|
||||||
appName: "MyAwesomeCalculator",
|
appName: "MyAwesome Calculator & Chat Manager",
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
log: (message) => {
|
log: (message) => {
|
||||||
print(`[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 cli = createCli(rootCommand, { globalContext: appContext });
|
||||||
const args = [...$vararg];
|
const args = [...$vararg];
|
||||||
cli(args);
|
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', 'show']); // Shows current config
|
||||||
cli(['config', 'set', 'theme', 'dark']); // Sets 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
|
// Help examples
|
||||||
cli(['--help']); // Shows root help
|
cli(['--help']); // Shows root help
|
||||||
cli(['math', '--help']); // Shows math command 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
|
// Debug mode
|
||||||
cli(['--debug', 'math', 'add', '1', '2']); // Enables debug logging
|
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> {
|
): Result<void, CliError> {
|
||||||
const { command, commandPath, options, remaining } = parseResult;
|
const { command, commandPath, options, remaining } = parseResult;
|
||||||
|
|
||||||
// Handle requests for help on a specific command.
|
// Unified Help Check:
|
||||||
if (shouldShowHelp([...remaining, ...Object.keys(options)])) {
|
// A command should show its help page if:
|
||||||
writer(generateHelp(command, commandPath));
|
// 1. A help flag is explicitly passed (`--help` or `-h`). This has the highest priority.
|
||||||
return Ok.EMPTY;
|
// 2. It's a command group that was called without a subcommand (i.e., it has no action).
|
||||||
}
|
const isHelpFlagPassed = shouldShowHelp([
|
||||||
|
...remaining,
|
||||||
// If a command has subcommands but no action, show its help page.
|
...Object.keys(options),
|
||||||
if (
|
]);
|
||||||
command.subcommands &&
|
const isCommandGroupWithoutAction =
|
||||||
|
command.subcommands !== undefined &&
|
||||||
command.subcommands.size > 0 &&
|
command.subcommands.size > 0 &&
|
||||||
command.action === undefined
|
command.action === undefined;
|
||||||
) {
|
|
||||||
|
if (isHelpFlagPassed || isCommandGroupWithoutAction) {
|
||||||
writer(generateHelp(command, commandPath));
|
writer(generateHelp(command, commandPath));
|
||||||
return Ok.EMPTY;
|
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) {
|
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({
|
return new Err({
|
||||||
kind: "NoAction",
|
kind: "NoAction",
|
||||||
commandPath: [...commandPath, command.name],
|
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)
|
return processArguments(command.args ?? [], remaining)
|
||||||
.andThen((args) => {
|
.andThen((args) => {
|
||||||
return processOptions(
|
return processOptions(
|
||||||
|
|||||||
@@ -103,5 +103,5 @@ export function generateCommandList<TContext extends object>(
|
|||||||
* @returns `true` if a help flag is found, otherwise `false`.
|
* @returns `true` if a help flag is found, otherwise `false`.
|
||||||
*/
|
*/
|
||||||
export function shouldShowHelp(argv: string[]): boolean {
|
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 currentCommand = rootCommand;
|
||||||
let i = 0;
|
|
||||||
let inOptions = false;
|
let inOptions = false;
|
||||||
|
|
||||||
const optionMapCache = new OptionMapCache();
|
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];
|
const arg = argv[i];
|
||||||
|
|
||||||
if (arg === undefined || arg === null) {
|
// Skip null/undefined arguments
|
||||||
i++;
|
if (!arg) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle double dash (--) - everything after is treated as a remaining argument
|
// Handle double dash (--) - everything after is treated as remaining
|
||||||
if (arg === "--") {
|
if (arg === "--") {
|
||||||
result.remaining.push(...argv.slice(i + 1));
|
result.remaining.push(...argv.slice(i + 1));
|
||||||
break;
|
break;
|
||||||
@@ -121,70 +148,33 @@ export function parseArguments<TContext extends object>(
|
|||||||
} else {
|
} else {
|
||||||
// --option [value] format
|
// --option [value] format
|
||||||
const optionName = arg.slice(2);
|
const optionName = arg.slice(2);
|
||||||
const optionDef = getCurrentOptionMaps().optionMap.get(optionName);
|
i = processOption(optionName, i);
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle short options (-o or -o value)
|
// Handle short options (-o or -o value)
|
||||||
else if (arg.startsWith("-") && arg.length > 1) {
|
else if (arg.startsWith("-") && arg.length > 1) {
|
||||||
inOptions = true;
|
inOptions = true;
|
||||||
const shortName = arg.slice(1);
|
const shortName = arg.slice(1);
|
||||||
|
const optionName =
|
||||||
// Get option maps for the new command (lazy loaded and cached)
|
currentOptionMaps.shortNameMap.get(shortName) ?? shortName;
|
||||||
const maps = getCurrentOptionMaps();
|
i = processOption(optionName, i);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Handle positional arguments and command resolution
|
// Handle positional arguments and command resolution
|
||||||
else {
|
else {
|
||||||
if (!inOptions) {
|
if (!inOptions) {
|
||||||
// Try to find this as a subcommand of the current command
|
// Try to find this as a subcommand of the current command
|
||||||
const subcommand = currentCommand.subcommands?.get(arg);
|
const subcommand = currentCommand.subcommands?.get(arg);
|
||||||
if (subcommand !== undefined) {
|
if (subcommand) {
|
||||||
// Found a subcommand, move deeper
|
updateCommand(subcommand, arg);
|
||||||
currentCommand = subcommand;
|
|
||||||
result.command = currentCommand;
|
|
||||||
result.commandPath.push(arg);
|
|
||||||
} else {
|
} else {
|
||||||
// Not a subcommand, treat as remaining argument
|
// Not a subcommand, treat as remaining argument
|
||||||
result.remaining.push(arg);
|
result.remaining.push(arg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// After options have started, treat as a remaining argument
|
// After options have started, treat as remaining argument
|
||||||
result.remaining.push(arg);
|
result.remaining.push(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Ok(result);
|
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"
|
| "light_purple"
|
||||||
| "yellow"
|
| "yellow"
|
||||||
| "white"
|
| "white"
|
||||||
| "reset"; // RGB color in #RRGGBB format
|
| `#${string}`;
|
||||||
|
|
||||||
declare type MinecraftFont =
|
declare type MinecraftFont =
|
||||||
| "minecraft:default"
|
| "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 pagedTabulate(...args: (LuaTable | Object | Color)[]): void;
|
||||||
function serialize(tab: object, options?: SerializeOptions): string;
|
function serialize(tab: object, options?: SerializeOptions): string;
|
||||||
function serialise(tab: object, options?: SerializeOptions): string;
|
function serialise(tab: object, options?: SerializeOptions): string;
|
||||||
function serializeJSON(tab: object, nbtStyle?: boolean): string;
|
function serializeJSON(
|
||||||
function serializeJSON(tab: object, options: SerializeJSONOptions): string;
|
tab: object | string | number | boolean,
|
||||||
function serialiseJSON(tab: object, nbtStyle?: boolean): string;
|
nbtStyle?: boolean,
|
||||||
function serialiseJSON(tab: object, options: SerializeJSONOptions): string;
|
): 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 unserialize(str: string): unknown;
|
||||||
function unserialise(str: string): unknown;
|
function unserialise(str: string): unknown;
|
||||||
function unserializeJSON(
|
function unserializeJSON(
|
||||||
|
|||||||
Reference in New Issue
Block a user