Compare commits

...

2 Commits

Author SHA1 Message Date
SikongJueluo
a4e74dcfa0 docs: ccCLI framework and ChatManager 2025-11-03 22:32:18 +08:00
SikongJueluo
2f57d9ab3d feature: accesscontrol welcome message 2025-11-03 13:20:21 +08:00
7 changed files with 730 additions and 200 deletions

View File

@@ -30,7 +30,17 @@ A declarative, reactive TUI (Terminal User Interface) framework inspired by [Sol
- **Control Flow:** Includes `<For>` and `<Show>` components for conditional and list-based rendering.
- **Component-Based:** Structure your UI into reusable components. See `src/tuiExample/main.ts` for a demo.
### 4. Core Libraries
### 4. ccCLI Framework
A lightweight, functional-style framework for building command-line interfaces (CLIs) within CC:Tweaked. It supports nested commands, arguments, options, and automatic help generation. See the [ccCLI Documentation](docs/ccCLI.md) for more details.
- **Declarative API:** Define commands, arguments, and options using a simple, object-based structure.
- **Nested Commands:** Organize complex applications with subcommands (e.g., `mycli command subcommand`).
- **Automatic Help:** Generates detailed help messages for commands and subcommands.
- **Global Context:** Inject shared state or services into command actions.
- **Type-Safe:** Built with TypeScript for robust development.
### 5. Core Libraries
- **`ChatManager`:** A powerful manager for `chatBox` peripherals that handles message queuing, cooldowns, and asynchronous sending/receiving. See the [ChatManager Documentation](docs/ChatManager.md) for more details.
- **`ccLog`:** A robust logging library with automatic, time-based log file rotation.
- **`PeripheralManager`:** A utility for easily finding and requiring peripherals by name or type.
- **`CraftManager`:** A library for parsing and executing crafting recipes from Create mod packages.

235
docs/ChatManager.md Normal file
View File

@@ -0,0 +1,235 @@
# ChatManager Documentation
## Introduction
`ChatManager` is a powerful utility for managing interactions with one or more `chatBox` peripherals in ComputerCraft. It simplifies the process of sending and receiving chat messages by handling complexities like peripheral cooldowns, message queuing, and asynchronous operations.
It is designed for applications that need to reliably send a high volume of messages or toasts without getting bogged down by peripheral limitations, or for applications that need to listen for commands or messages from players.
## Features
* **Multi-Peripheral Management:** Seamlessly manages one or more `chatBox` peripherals.
* **Message Queuing:** Automatically queues messages and toasts, sending them as chatboxes become available.
* **Cooldown Handling:** Respects the 1-second cooldown of chatboxes to prevent message loss.
* **Asynchronous Operation:** Can run in the background (`runAsync`) without blocking your main program loop.
* **Message Buffering:** Receives and buffers incoming chat messages for your application to process.
* **Queued and Immediate Sending:** Supports both adding messages to a queue and sending them immediately (if a chatbox is available).
* **Rich Content Support:** Send simple strings or complex formatted messages using `MinecraftTextComponent`.
* **Robust Error Handling:** Uses a `Result`-based API to make error handling explicit and reliable.
* **Comprehensive API:** Provides methods for sending global messages, private messages, and toast notifications.
## Tutorial: Getting Started with ChatManager
Heres how to integrate `ChatManager` into your project.
### 1. Initialization
First, find your available `chatBox` peripherals and create a `ChatManager` instance.
```typescript
import { ChatManager } from '@/lib/ChatManager';
// 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) {
print("Error: No chatbox peripherals found.");
return;
}
// Create the manager instance
const chatManager = new ChatManager(chatboxPeripherals);
```
### 2. Running the Manager
To start the sending and receiving loops, you must run the manager. For most use cases, running it asynchronously is best.
```typescript
// Start ChatManager in the background so it doesn't block the main program
const runResult = chatManager.runAsync();
if (runResult.isErr()) {
print(`Warning: Failed to start ChatManager: ${runResult.error.reason}`);
} else {
print("ChatManager started successfully!");
}
// Your main program logic can continue here...
```
**Important:** `ChatManager` relies on `gTimerManager` to handle cooldowns. Ensure you are also running the global timer manager in your application.
```typescript
import { gTimerManager } from "@/lib/TimerManager";
// In your main parallel loop
parallel.waitForAll(
() => yourMainLoop(),
() => chatManager.run(), // if you choose the blocking run
() => gTimerManager.run()
);
```
### 3. Sending a Message (Queued)
Use `sendMessage` to add a message to the queue. `ChatManager` will send it as soon as a chatbox is free.
```typescript
// Send a global message
chatManager.sendMessage({
message: "Hello, world!",
prefix: "MySystem",
});
// Send a private message
chatManager.sendMessage({
message: "This is a secret.",
targetPlayer: "Steve",
prefix: "Whisper",
});
```
### 4. Sending a Toast (Queued)
Similarly, use `sendToast` to queue a toast notification.
```typescript
chatManager.sendToast({
title: "Server Alert",
message: "Restart in 5 minutes!",
targetPlayer: "Steve",
prefix: "Admin",
});
```
### 5. Receiving Messages
Use `getReceivedMessage` to pull incoming chat events from the buffer. It's best to do this in a loop.
```typescript
function myCliLoop() {
while (true) {
const result = chatManager.getReceivedMessage();
if (result.isOk()) {
const event = result.value;
print(`[${event.username}]: ${event.message}`);
// Process the command or message...
} else {
// Buffer is empty, wait a bit before checking again
sleep(0.5);
}
}
}
```
## Advanced Topics
### Immediate Sending
If you need to send a message right away and bypass the queue, use the `Immediate` methods. These will fail if no chatbox is currently available.
```typescript
const result = chatManager.sendMessageImmediate({
message: "URGENT!",
targetPlayer: "Admin",
});
if (result.isErr()) {
if (result.error.kind === "NoIdleChatbox") {
print("Could not send immediately: all chatboxes are busy.");
} else {
print(`Failed to send message: ${result.error.reason}`);
}
}
```
### Rich Text Messages (`MinecraftTextComponent`)
You can send fully formatted messages by providing a `MinecraftTextComponent` object instead of a string.
```typescript
const richMessage: MinecraftTextComponent = {
text: "This is ",
color: "gold",
extra: [
{ text: "important!", color: "red", bold: true }
],
};
chatManager.sendMessage({
message: richMessage,
targetPlayer: "AllPlayers",
utf8Support: true, // Recommended for complex components
});
```
### Error Handling
Methods return a `Result` object (`Ok` or `Err`). Always check the result to handle potential failures gracefully.
```typescript
const result = chatManager.sendMessage(message);
if (result.isErr()) {
logger.error(`Failed to queue message: ${result.error.reason}`);
}
```
The possible error `kind`s are:
* `ChatManagerError`: General errors, e.g., failure to enqueue.
* `NoIdleChatboxError`: Returned by `Immediate` methods when no chatbox is free.
* `SendFailureError`: A hardware or permission error occurred during sending.
* `EmptyBufferError`: Returned by `getReceivedMessage` when the buffer is empty.
### Status and Management
You can inspect and control the `ChatManager` at runtime.
```typescript
// Get the number of items waiting to be sent
const pending = chatManager.getPendingMessageCount();
print(`Messages in queue: ${pending}`);
// Get the number of received messages waiting to be processed
const buffered = chatManager.getBufferedMessageCount();
print(`Received messages in buffer: ${buffered}`);
// Get the status of each chatbox (true = idle, false = busy)
const statuses = chatManager.getChatboxStatus();
// Clear the sending queues
chatManager.clearQueues();
// Clear the received message buffer
chatManager.clearBuffer();
// Stop the manager's background loops
chatManager.stop();
```
## API Reference
### Core Class
* `ChatManager(peripherals: ChatBoxPeripheral[])`
### Primary Methods
* `run(): Result<void, ChatManagerError>`: Starts the manager (blocking).
* `runAsync(): Result<LuaThread, ChatManagerError>`: Starts the manager in the background.
* `stop(): Result<void, ChatManagerError>`: Stops the background loops.
* `sendMessage(message: ChatMessage): Result<void, ChatManagerError>`: Queues a chat message.
* `sendToast(toast: ChatToast): Result<void, ChatManagerError>`: Queues a toast.
* `getReceivedMessage(): Result<ChatBoxEvent, EmptyBufferError>`: Retrieves a message from the receive buffer.
* `sendMessageImmediate(message: ChatMessage): Result<void, ChatError>`: Sends a message immediately.
* `sendToastImmediate(toast: ChatToast): Result<void, ChatError>`: Sends a toast immediately.
### Interfaces
* `ChatMessage`
* `ChatToast`
* `ChatError` (union of all possible error types)

248
docs/ccCLI.md Normal file
View File

@@ -0,0 +1,248 @@
# ccCLI Framework Documentation
## Introduction
`ccCLI` is a lightweight, functional-style framework for building command-line interfaces (CLIs) within the CC:Tweaked environment using TSTL (TypeScriptToLua). It provides a declarative and type-safe way to define commands, arguments, and options, with built-in support for nested commands, automatic help generation, and robust error handling.
Its design is inspired by modern CLI libraries and emphasizes simplicity and ease of use, allowing developers to quickly structure complex command-based applications.
## Features
* **Declarative API:** Define commands as simple objects.
* **Type-Safe:** Leverage TypeScript for defining commands, arguments, options, and context.
* **Nested Commands:** Easily create command groups and subcommands (e.g., `git remote add`).
* **Automatic Help Generation:** Generates `--help` messages for the root command and all subcommands.
* **Flexible Argument & Option Parsing:** Supports long names (`--verbose`), short names (`-v`), value assignment (`--file=path.txt`), and boolean flags.
* **Global Context Injection:** Share state, services, or configuration across all commands.
* **Result-Based Error Handling:** Command actions return a `Result` type, ensuring that errors are handled explicitly.
* **No Dependencies:** Written in pure TypeScript with no external runtime dependencies.
## Core Concepts
The framework is built around a few key interfaces:
* `Command<TContext>`: The central piece. It defines a command's name, description, arguments, options, subcommands, and the action to perform.
* `Argument`: Defines a positional argument for a command. It can be marked as required.
* `Option`: Defines a named option (flag). It can have a long name, a short name, a default value, and be marked as required.
* `ActionContext<TContext>`: The object passed to every command's `action` function. It contains the parsed `args`, `options`, and the shared `context` object.
## Tutorial: Creating a Simple Calculator CLI
Let's build a simple calculator to see how `ccCLI` works.
### 1. Define the Global Context (Optional)
The global context is a powerful feature for sharing data or services. Let's define a context for our app.
```typescript
// src/cliExample/main.ts
interface AppContext {
appName: string;
log: (message: string) => void;
debugMode: boolean;
}
```
### 2. Define Commands
Commands are just JavaScript objects. The logic goes into the `action` function.
```typescript
// src/cliExample/main.ts
import { Command, CliError } from "../lib/ccCLI/index";
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
const addCommand: Command<AppContext> = {
name: "add",
description: "Adds two numbers together",
args: [
{ name: "a", description: "The first number", required: true },
{ name: "b", description: "The second number", required: true },
],
action: ({ args, context }): Result<void, CliError> => {
context.log(`Executing 'add' command in '${context.appName}'`);
const a = tonumber(args.a as string);
const b = tonumber(args.b as string);
if (a === undefined || b === undefined) {
print("Error: Arguments must be numbers.");
return Ok.EMPTY;
}
const result = a + b;
print(`${a} + ${b} = ${result}`);
return Ok.EMPTY;
},
};
```
### 3. Create Nested Commands
You can group commands under a parent command using the `subcommands` property.
```typescript
// src/cliExample/main.ts
// (addCommand is defined above, subtractCommand would be similar)
const mathCommand: Command<AppContext> = {
name: "math",
description: "Mathematical operations",
subcommands: new Map([
["add", addCommand],
["subtract", subtractCommand], // Assuming subtractCommand is defined
]),
};
```
If a command with subcommands is called without an action, it will automatically display its help page.
### 4. Define the Root Command
The root command is the entry point for your entire application. It contains all top-level commands and global options.
```typescript
// src/cliExample/main.ts
const rootCommand: Command<AppContext> = {
name: "calculator",
description: "A feature-rich calculator program",
options: new Map([
[
"debug",
{
name: "debug",
shortName: "d",
description: "Enable debug mode",
defaultValue: false,
},
],
]),
subcommands: new Map([
["math", mathCommand],
// other commands...
]),
action: ({ context }) => {
print(`Welcome to ${context.appName}!`);
print("Use --help to see available commands");
return Ok.EMPTY;
},
};
```
### 5. Create and Run the CLI
Finally, create the context instance and pass it along with the root command to `createCli`. This returns a handler function that you can call with the program's arguments.
```typescript
// src/cliExample/main.ts
import { createCli } from "../lib/ccCLI/index";
// Create global context instance
const appContext: AppContext = {
appName: "MyAwesome Calculator",
debugMode: false,
log: (message) => {
if (appContext.debugMode) {
print(`[LOG] ${message}`);
}
},
};
// Create the CLI handler
const cli = createCli(rootCommand, { globalContext: appContext });
// Get arguments and run
const args = [...$vararg];
cli(args);
```
### Usage
You can now run your CLI from the ComputerCraft terminal:
```sh
> lua program.lua math add 5 7
12
> lua program.lua --debug math add 5 7
[LOG] Executing 'add' command in 'MyAwesome Calculator'
12
> lua program.lua math --help
# Displays help for the 'math' command
```
## Advanced Topics
### Arguments
Arguments are positional values passed after a command. They are defined in an array.
```typescript
args: [
{ name: "a", description: "The first number", required: true },
{ name: "b", description: "The second number" }, // optional
],
```
### Options
Options are named values (flags) that can appear anywhere. They are defined in a `Map`.
```typescript
options: new Map([
[
"name", // The key in the map must match the option's name
{
name: "name",
shortName: "n",
description: "The name to greet",
defaultValue: "World",
},
],
[
"force",
{
name: "force",
description: "Force the operation",
defaultValue: false, // For boolean flags
},
],
]),
```
They can be used like this:
* `--name "John"` or `-n "John"`
* `--name="John"`
* `--force` (sets the value to `true`)
### Error Handling
The `action` function must return a `Result<void, CliError>`.
* Return `Ok.EMPTY` on success.
* The framework automatically handles parsing errors like missing arguments or unknown commands. You can return your own errors from within an action if needed, though this is less common. The primary mechanism is simply printing an error message and returning `Ok.EMPTY`.
## API Reference
The public API is exposed through `src/lib/ccCLI/index.ts`.
### Core Function
* `createCli<TContext>(rootCommand, options)`: Creates the main CLI handler function.
* `rootCommand`: The top-level command of your application.
* `options.globalContext`: The context object to be injected into all actions.
* `options.writer`: An optional function to handle output (defaults to `textutils.pagedPrint`).
### Core Types
* `Command<TContext>`
* `Argument`
* `Option`
* `ActionContext<TContext>`
* `CliError`
This documentation provides a comprehensive overview of the `ccCLI` framework. By following the tutorial and referencing the examples, you can build powerful and well-structured command-line tools for CC:Tweaked.

View File

@@ -117,8 +117,8 @@ const delCommand: Command<AppContext> = {
},
};
const listCommand: Command<AppContext> = {
name: "list",
const listUserCommand: Command<AppContext> = {
name: "user",
description: "列出所有玩家及其所在的用户组",
action: ({ context }) => {
const config = loadConfig(context.configFilepath)!;
@@ -134,213 +134,227 @@ const listCommand: Command<AppContext> = {
},
};
const setCommand: Command<AppContext> = {
name: "set",
const listGroupCommand: Command<AppContext> = {
name: "group",
description: "显示详细的用户组配置信息",
action: ({ context }) => {
const config = loadConfig(context.configFilepath)!;
let groupsMessage = `管理员组: ${config.adminGroupConfig.groupName}\n`;
groupsMessage += ` 用户: [${config.adminGroupConfig.groupUsers.join(
", ",
)}]\n`;
groupsMessage += ` 允许: ${config.adminGroupConfig.isAllowed}\n`;
groupsMessage += ` 通知: ${config.adminGroupConfig.isNotice}\n\n`;
for (const group of config.usersGroups) {
groupsMessage += `用户组: ${group.groupName}\n`;
groupsMessage += ` 用户: [${(group.groupUsers ?? []).join(", ")}]\n`;
groupsMessage += ` 允许: ${group.isAllowed}\n`;
groupsMessage += ` 通知: ${group.isNotice}\n`;
groupsMessage += "\n";
}
context.print({ text: groupsMessage.trim() });
return Ok.EMPTY;
},
};
const listToastCommand: Command<AppContext> = {
name: "toast",
description: "显示 Toast 配置信息",
action: ({ context }) => {
const config = loadConfig(context.configFilepath)!;
let toastMessage = "默认 Toast 配置:\n";
toastMessage += ` 标题: ${config.welcomeToastConfig.title.text}\n`;
toastMessage += ` 消息: ${config.welcomeToastConfig.msg.text}\n`;
toastMessage += ` 前缀: ${config.welcomeToastConfig.prefix ?? "none"}\n`;
toastMessage += ` 括号: ${config.welcomeToastConfig.brackets ?? "none"}\n`;
toastMessage += ` 括号颜色: ${
config.welcomeToastConfig.bracketColor ?? "none"
}\n\n`;
toastMessage += "警告 Toast 配置:\n";
toastMessage += ` 标题: ${config.warnToastConfig.title.text}\n`;
toastMessage += ` 消息: ${config.warnToastConfig.msg.text}\n`;
toastMessage += ` 前缀: ${config.warnToastConfig.prefix ?? "none"}\n`;
toastMessage += ` 括号: ${config.warnToastConfig.brackets ?? "none"}\n`;
toastMessage += ` 括号颜色: ${
config.warnToastConfig.bracketColor ?? "none"
}`;
context.print({ text: toastMessage });
return Ok.EMPTY;
},
};
const listAllCommand: Command<AppContext> = {
name: "all",
description: "显示基本配置信息概览",
action: ({ context }) => {
const config = loadConfig(context.configFilepath)!;
let allMessage = `检测范围: ${config.detectRange}\n`;
allMessage += `检测间隔: ${config.detectInterval}\n`;
allMessage += `警告间隔: ${config.watchInterval}\n`;
allMessage += `通知次数: ${config.noticeTimes}\n`;
allMessage += `全局欢迎功能: ${config.isWelcome}\n`;
allMessage += `全局警告功能: ${config.isWarn}\n\n`;
allMessage += "使用 'list group' 或 'list toast' 查看详细信息";
context.print({ text: allMessage });
return Ok.EMPTY;
},
};
const listCommand: Command<AppContext> = {
name: "list",
description: "列出玩家、组信息或配置",
subcommands: new Map([
["user", listUserCommand],
["group", listGroupCommand],
["toast", listToastCommand],
["all", listAllCommand],
]),
action: ({ context }) => {
const config = loadConfig(context.configFilepath)!;
let allMessage = `检测范围: ${config.detectRange}\n`;
allMessage += `检测间隔: ${config.detectInterval}\n`;
allMessage += `警告间隔: ${config.watchInterval}\n`;
allMessage += `通知次数: ${config.noticeTimes}\n`;
allMessage += `全局欢迎功能: ${config.isWelcome}\n`;
allMessage += `全局警告功能: ${config.isWarn}\n\n`;
allMessage += "使用 'list group' 或 'list toast' 查看详细信息";
context.print({ text: allMessage });
return Ok.EMPTY;
},
};
const configCommand: Command<AppContext> = {
name: "config",
description: "配置访问控制设置",
args: [
{
name: "option",
description: "要设置的选项 (warnInterval, detectInterval, detectRange)",
description:
"要设置的选项 (warnInterval, detectInterval, detectRange, noticeTimes, isWelcome, isWarn) 或用户组属性 (<groupName>.isAllowed, <groupName>.isNotice, <groupName>.isWelcome)",
required: true,
},
{ name: "value", description: "要设置的值", required: true },
],
action: ({ args, context }) => {
const [option, valueStr] = [args.option as string, args.value as string];
const value = parseInt(valueStr);
if (isNaN(value)) {
context.print({ text: `无效的值: ${valueStr}. 必须是一个数字。` });
return Ok.EMPTY;
}
const config = loadConfig(context.configFilepath)!;
let message = "";
switch (option) {
case "warnInterval":
config.watchInterval = value;
message = `已设置警告间隔为 ${value}`;
break;
case "detectInterval":
config.detectInterval = value;
message = `已设置检测间隔为 ${value}`;
break;
case "detectRange":
config.detectRange = value;
message = `已设置检测范围为 ${value}`;
break;
default:
// Check if it's a group property (contains a dot)
if (option.includes(".")) {
const dotIndex = option.indexOf(".");
const groupName = option.substring(0, dotIndex);
const property = option.substring(dotIndex + 1);
let groupConfig: UserGroupConfig | undefined;
if (groupName === "admin") {
groupConfig = config.adminGroupConfig;
} else {
groupConfig = config.usersGroups.find((g) => g.groupName === groupName);
}
if (!groupConfig) {
context.print({ text: `用户组 ${groupName} 未找到` });
return Ok.EMPTY;
}
const boolValue = parseBoolean(valueStr);
if (boolValue === undefined) {
context.print({
text: `未知选项: ${option}. 可用选项: warnInterval, detectInterval, detectRange`,
text: `无效的布尔值: ${valueStr}. 请使用 'true' 或 'false'.`,
});
return Ok.EMPTY;
}
}
saveConfig(config, context.configFilepath);
context.reloadConfig();
context.print({ text: message });
return Ok.EMPTY;
},
};
let message = "";
switch (property) {
case "isAllowed":
groupConfig.isAllowed = boolValue;
message = `已设置 ${groupName}.isAllowed 为 ${boolValue}`;
break;
case "isNotice":
groupConfig.isNotice = boolValue;
message = `已设置 ${groupName}.isNotice 为 ${boolValue}`;
break;
case "isWelcome":
groupConfig.isWelcome = boolValue;
message = `已设置 ${groupName}.isWelcome 为 ${boolValue}`;
break;
default:
context.print({
text: `未知属性: ${property}. 可用属性: isAllowed, isNotice, isWelcome`,
});
return Ok.EMPTY;
}
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)!;
let groupConfig: UserGroupConfig | undefined;
if (groupName === "admin") {
groupConfig = config.adminGroupConfig;
saveConfig(config, context.configFilepath);
context.reloadConfig();
context.print({ text: message });
return Ok.EMPTY;
} else {
groupConfig = config.usersGroups.find((g) => g.groupName === groupName);
}
// Handle basic configuration options
let message = "";
if (!groupConfig) {
context.print({ text: `用户组 ${groupName} 未找到` });
return Ok.EMPTY;
}
const boolValue = parseBoolean(valueStr);
if (boolValue === undefined) {
context.print({
text: `无效的布尔值: ${valueStr}. 请使用 'true' 或 'false'.`,
});
return Ok.EMPTY;
}
let message = "";
switch (property) {
case "isAllowed":
groupConfig.isAllowed = boolValue;
message = `已设置 ${groupName}.isAllowed ${boolValue}`;
break;
case "isNotice":
groupConfig.isNotice = boolValue;
message = `已设置 ${groupName}.isNotice 为 ${boolValue}`;
break;
default:
context.print({
text: `未知属性: ${property}. 可用属性: isAllowed, isNotice`,
});
return Ok.EMPTY;
}
saveConfig(config, context.configFilepath);
context.reloadConfig();
context.print({ text: message });
return Ok.EMPTY;
},
};
const editCommand: Command<AppContext> = {
name: "edit",
description: "编辑各项配置",
subcommands: new Map([["group", editGroupCommand]]),
};
const showConfigCommand: Command<AppContext> = {
name: "showconfig",
description: "显示配置",
options: new Map([
[
"type",
{
name: "type",
description: "要显示的配置类型 (groups, toast, all)",
required: false,
defaultValue: "all",
},
],
]),
action: ({ options, context }) => {
const type = options.type as string;
const config = loadConfig(context.configFilepath)!;
let message = "";
switch (type) {
case "groups": {
let groupsMessage = `管理员组: ${config.adminGroupConfig.groupName}\n`;
groupsMessage += ` 用户: [${config.adminGroupConfig.groupUsers.join(
", ",
)}]\n`;
groupsMessage += ` 允许: ${config.adminGroupConfig.isAllowed}\n`;
groupsMessage += ` 通知: ${config.adminGroupConfig.isNotice}\n\n`;
for (const group of config.usersGroups) {
groupsMessage += `用户组: ${group.groupName}\n`;
groupsMessage += ` 用户: [${(group.groupUsers ?? []).join(", ")}]\n`;
groupsMessage += ` 允许: ${group.isAllowed}\n`;
groupsMessage += ` 通知: ${group.isNotice}\n`;
groupsMessage += "\n";
// Check if it's a boolean option
if (option === "isWelcome" || option === "isWarn") {
const boolValue = parseBoolean(valueStr);
if (boolValue === undefined) {
context.print({
text: `无效的布尔值: ${valueStr}. 请使用 'true' 或 'false'.`,
});
return Ok.EMPTY;
}
switch (option) {
case "isWelcome":
config.isWelcome = boolValue;
message = `已设置全局欢迎功能为 ${boolValue}`;
break;
case "isWarn":
config.isWarn = boolValue;
message = `已设置全局警告功能${boolValue}`;
break;
}
} else {
// Handle numeric options
const value = parseInt(valueStr);
if (isNaN(value)) {
context.print({ text: `无效的值: ${valueStr}. 必须是一个数字。` });
return Ok.EMPTY;
}
switch (option) {
case "warnInterval":
config.watchInterval = value;
message = `已设置警告间隔为 ${value}`;
break;
case "detectInterval":
config.detectInterval = value;
message = `已设置检测间隔为 ${value}`;
break;
case "detectRange":
config.detectRange = value;
message = `已设置检测范围为 ${value}`;
break;
case "noticeTimes":
config.noticeTimes = value;
message = `已设置通知次数为 ${value}`;
break;
default:
context.print({
text: `未知选项: ${option}. 可用选项: warnInterval, detectInterval, detectRange, noticeTimes, isWelcome, isWarn 或 <groupName>.isAllowed, <groupName>.isNotice, <groupName>.isWelcome`,
});
return Ok.EMPTY;
}
message = groupsMessage.trim();
break;
}
case "toast": {
let toastMessage = "默认 Toast 配置:\n";
toastMessage += ` 标题: ${config.welcomeToastConfig.title.text}\n`;
toastMessage += ` 消息: ${config.welcomeToastConfig.msg.text}\n`;
toastMessage += ` 前缀: ${
config.welcomeToastConfig.prefix ?? "none"
}\n`;
toastMessage += ` 括号: ${
config.welcomeToastConfig.brackets ?? "none"
}\n`;
toastMessage += ` 括号颜色: ${
config.welcomeToastConfig.bracketColor ?? "none"
}\n\n`;
toastMessage += "警告 Toast 配置:\n";
toastMessage += ` 标题: ${config.warnToastConfig.title.text}\n`;
toastMessage += ` 消息: ${config.warnToastConfig.msg.text}\n`;
toastMessage += ` 前缀: ${config.warnToastConfig.prefix ?? "none"}\n`;
toastMessage += ` 括号: ${
config.warnToastConfig.brackets ?? "none"
}\n`;
toastMessage += ` 括号颜色: ${
config.warnToastConfig.bracketColor ?? "none"
}`;
message = toastMessage;
break;
}
case "all": {
let allMessage = `检测范围: ${config.detectRange}\n`;
allMessage += `检测间隔: ${config.detectInterval}\n`;
allMessage += `警告间隔: ${config.watchInterval}\n\n`;
allMessage +=
"使用 'showconfig --type groups' 或 'showconfig --type toast' 查看详细信息";
message = allMessage;
break;
}
default:
message = `无效类型: ${type}. 可用类型: groups, toast, all`;
break;
saveConfig(config, context.configFilepath);
context.reloadConfig();
context.print({ text: message });
return Ok.EMPTY;
}
context.print({ text: message });
return Ok.EMPTY;
},
};
@@ -352,9 +366,7 @@ const rootCommand: Command<AppContext> = {
["add", addCommand],
["del", delCommand],
["list", listCommand],
["set", setCommand],
["edit", editCommand],
["showconfig", showConfigCommand],
["config", configCommand],
]),
action: ({ context }) => {
context.print([
@@ -383,6 +395,6 @@ const rootCommand: Command<AppContext> = {
export function createAccessControlCli(context: AppContext) {
return createCli(rootCommand, {
globalContext: context,
writer: (msg) => context.print(msg),
writer: (msg) => context.print({ text: msg }),
});
}

View File

@@ -21,6 +21,7 @@ interface AccessConfig {
watchInterval: number;
noticeTimes: number;
detectRange: number;
isWelcome: boolean;
isWarn: boolean;
adminGroupConfig: UserGroupConfig;
welcomeToastConfig: ToastConfig;
@@ -35,12 +36,13 @@ const defaultConfig: AccessConfig = {
watchInterval: 10,
noticeTimes: 2,
isWarn: false,
isWelcome: true,
adminGroupConfig: {
groupName: "Admin",
groupUsers: ["Selcon"],
isAllowed: true,
isNotice: true,
isWelcome: true,
isWelcome: false,
},
usersGroups: [
{
@@ -50,6 +52,13 @@ const defaultConfig: AccessConfig = {
isNotice: true,
isWelcome: false,
},
{
groupName: "TU",
groupUsers: [],
isAllowed: true,
isNotice: false,
isWelcome: false,
},
{
groupName: "VIP",
groupUsers: [],

View File

@@ -2,7 +2,6 @@ import { CCLog, DAY, LogLevel } from "@/lib/ccLog";
import { ToastConfig, UserGroupConfig, loadConfig } from "./config";
import { createAccessControlCli } from "./cli";
import { launchAccessControlTUI } from "./tui";
import * as peripheralManager from "../lib/PeripheralManager";
import { deepCopy } from "@/lib/common";
import { ReadWriteLock } from "@/lib/mutex/ReadWriteLock";
import { ChatManager } from "@/lib/ChatManager";
@@ -15,7 +14,7 @@ const args = [...$vararg];
const logger = new CCLog("accesscontrol.log", {
printTerminal: true,
logInterval: DAY,
outputMinLevel: LogLevel.Debug,
outputMinLevel: LogLevel.Info,
});
// Load Config
@@ -278,6 +277,8 @@ function mainLoop() {
isNotice: false,
isWelcome: false,
};
// Get user group config
for (const userGroupConfig of config.usersGroups) {
if (userGroupConfig.groupUsers == undefined) continue;
if (!userGroupConfig.groupUsers.includes(player)) continue;
@@ -286,21 +287,27 @@ function mainLoop() {
logger.info(
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
if (userGroupConfig.isWelcome)
sendMessage(config.welcomeToastConfig, player, {
playerName: player,
groupName: groupConfig.groupName,
info: playerInfo,
});
break;
}
if (config.adminGroupConfig.isWelcome)
sendMessage(config.welcomeToastConfig, player, {
playerName: player,
groupName: groupConfig.groupName,
info: playerInfo,
});
if (groupConfig.isAllowed) continue;
logger.warn(
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
if (config.isWelcome)
sendMessage(config.welcomeToastConfig, player, {
playerName: player,
groupName: groupConfig.groupName,
info: playerInfo,
});
if (config.isWarn) sendWarn(player);
gWatchPlayersInfo = [
...gWatchPlayersInfo,

View File

@@ -323,6 +323,15 @@ const AccessControlTUI = () => {
onChange: (checked) => setConfig("isWarn", checked),
}),
),
div(
{ class: "flex flex-row" },
label({}, "Is Welcome:"),
input({
type: "checkbox",
checked: () => config().isWelcome ?? false,
onChange: (checked) => setConfig("isWelcome", checked),
}),
),
);
};