mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-29 12:57:50 +08:00
feature: ChatManager
feature: - multi chatboxes manage - chatbox message queue
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
/**
|
||||
* Example CLI application demonstrating the ccCLI framework
|
||||
* This example shows how to create a calculator CLI with global context injection
|
||||
* and ChatManager integration for Minecraft chat functionality
|
||||
*/
|
||||
|
||||
import { Command, createCli, CliError } from "../lib/ccCLI/index";
|
||||
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
|
||||
import { ChatManager, ChatMessage, ChatToast } from "../lib/ChatManager";
|
||||
|
||||
// 1. Define global context type
|
||||
interface AppContext {
|
||||
appName: string;
|
||||
log: (message: string) => void;
|
||||
debugMode: boolean;
|
||||
chatManager?: ChatManager;
|
||||
}
|
||||
|
||||
// 2. Define individual commands
|
||||
@@ -153,10 +156,346 @@ const configCommand: Command<AppContext> = {
|
||||
]),
|
||||
};
|
||||
|
||||
// ChatManager commands
|
||||
const chatSendCommand: Command<AppContext> = {
|
||||
name: "send",
|
||||
description: "Send a chat message",
|
||||
args: [
|
||||
{ name: "message", description: "The message to send", required: true },
|
||||
],
|
||||
options: new Map([
|
||||
[
|
||||
"player",
|
||||
{
|
||||
name: "player",
|
||||
shortName: "p",
|
||||
description: "Target player for private message",
|
||||
defaultValue: undefined,
|
||||
},
|
||||
],
|
||||
[
|
||||
"prefix",
|
||||
{
|
||||
name: "prefix",
|
||||
description: "Message prefix",
|
||||
defaultValue: "CC",
|
||||
},
|
||||
],
|
||||
]),
|
||||
action: ({ args, options, context }): Result<void, CliError> => {
|
||||
if (!context.chatManager) {
|
||||
print(
|
||||
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||
);
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
const message: ChatMessage = {
|
||||
message: args.message as string,
|
||||
targetPlayer: options.player as string | undefined,
|
||||
prefix: options.prefix as string,
|
||||
};
|
||||
|
||||
const result = context.chatManager.sendMessage(message);
|
||||
if (result.isOk()) {
|
||||
print(`Message queued: "${String(args.message)}"`);
|
||||
|
||||
const targetPlayer = options.player;
|
||||
if (
|
||||
targetPlayer !== undefined &&
|
||||
targetPlayer !== null &&
|
||||
typeof targetPlayer === "string"
|
||||
) {
|
||||
print(`Target: ${targetPlayer}`);
|
||||
} else {
|
||||
print("Target: Global chat");
|
||||
}
|
||||
} else {
|
||||
print(`Failed to queue message: ${result.error.reason}`);
|
||||
}
|
||||
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
const chatToastCommand: Command<AppContext> = {
|
||||
name: "toast",
|
||||
description: "Send a toast notification to a player",
|
||||
args: [
|
||||
{ name: "player", description: "Target player username", required: true },
|
||||
{ name: "title", description: "Toast title", required: true },
|
||||
{ name: "message", description: "Toast message", required: true },
|
||||
],
|
||||
options: new Map([
|
||||
[
|
||||
"prefix",
|
||||
{
|
||||
name: "prefix",
|
||||
description: "Message prefix",
|
||||
defaultValue: "CC",
|
||||
},
|
||||
],
|
||||
]),
|
||||
action: ({ args, options, context }): Result<void, CliError> => {
|
||||
if (!context.chatManager) {
|
||||
print(
|
||||
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||
);
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
const toast: ChatToast = {
|
||||
username: args.player as string,
|
||||
title: args.title as string,
|
||||
message: args.message as string,
|
||||
prefix: options.prefix as string,
|
||||
};
|
||||
|
||||
const result = context.chatManager.sendToast(toast);
|
||||
if (result.isOk()) {
|
||||
print(
|
||||
`Toast queued for ${String(args.player)}: "${String(args.title)}" - "${String(args.message)}"`,
|
||||
);
|
||||
} else {
|
||||
print(`Failed to queue toast: ${result.error.reason}`);
|
||||
}
|
||||
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
const chatStatusCommand: Command<AppContext> = {
|
||||
name: "status",
|
||||
description: "Show ChatManager status and queue information",
|
||||
action: ({ context }): Result<void, CliError> => {
|
||||
if (!context.chatManager) {
|
||||
print("ChatManager: Not initialized (no chatbox peripherals found)");
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
print("=== ChatManager Status ===");
|
||||
print(`Pending messages: ${context.chatManager.getPendingMessageCount()}`);
|
||||
print(`Pending toasts: ${context.chatManager.getPendingToastCount()}`);
|
||||
print(
|
||||
`Buffered received: ${context.chatManager.getBufferedMessageCount()}`,
|
||||
);
|
||||
|
||||
const chatboxStatus = context.chatManager.getChatboxStatus();
|
||||
print(`Chatboxes: ${chatboxStatus.length} total`);
|
||||
|
||||
for (let i = 0; i < chatboxStatus.length; i++) {
|
||||
const status = chatboxStatus[i] ? "idle" : "busy";
|
||||
print(` Chatbox ${i + 1}: ${status}`);
|
||||
}
|
||||
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
const chatReceiveCommand: Command<AppContext> = {
|
||||
name: "receive",
|
||||
description: "Check for received chat messages",
|
||||
options: new Map([
|
||||
[
|
||||
"count",
|
||||
{
|
||||
name: "count",
|
||||
shortName: "c",
|
||||
description: "Number of messages to retrieve",
|
||||
defaultValue: 1,
|
||||
},
|
||||
],
|
||||
]),
|
||||
action: ({ options, context }): Result<void, CliError> => {
|
||||
if (!context.chatManager) {
|
||||
print(
|
||||
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||
);
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
const count = tonumber(options.count as string) ?? 1;
|
||||
let retrieved = 0;
|
||||
|
||||
print("=== Received Messages ===");
|
||||
for (let i = 0; i < count; i++) {
|
||||
const result = context.chatManager.getReceivedMessage();
|
||||
if (result.isOk()) {
|
||||
const event = result.value;
|
||||
print(`[${event.username}]: ${event.message}`);
|
||||
if (event.uuid !== undefined) {
|
||||
print(` UUID: ${event.uuid}`);
|
||||
}
|
||||
retrieved++;
|
||||
} else {
|
||||
// Buffer is empty
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (retrieved === 0) {
|
||||
print("No messages in buffer");
|
||||
} else {
|
||||
print(`Retrieved ${retrieved} message(s)`);
|
||||
}
|
||||
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
const chatSendImmediateCommand: Command<AppContext> = {
|
||||
name: "send-immediate",
|
||||
description: "Send a chat message immediately (bypass queue)",
|
||||
args: [
|
||||
{ name: "message", description: "The message to send", required: true },
|
||||
],
|
||||
options: new Map([
|
||||
[
|
||||
"player",
|
||||
{
|
||||
name: "player",
|
||||
shortName: "p",
|
||||
description: "Target player for private message",
|
||||
defaultValue: undefined,
|
||||
},
|
||||
],
|
||||
[
|
||||
"prefix",
|
||||
{
|
||||
name: "prefix",
|
||||
description: "Message prefix",
|
||||
defaultValue: "CC",
|
||||
},
|
||||
],
|
||||
]),
|
||||
action: ({ args, options, context }): Result<void, CliError> => {
|
||||
if (!context.chatManager) {
|
||||
print(
|
||||
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||
);
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
const message: ChatMessage = {
|
||||
message: args.message as string,
|
||||
targetPlayer: options.player as string | undefined,
|
||||
prefix: options.prefix as string,
|
||||
};
|
||||
|
||||
const result = context.chatManager.sendMessageImmediate(message);
|
||||
if (result.isOk()) {
|
||||
print(`Message sent immediately: "${String(args.message)}"`);
|
||||
} else {
|
||||
print(`Failed to send message: ${result.error.reason}`);
|
||||
if (result.error.kind === "NoIdleChatbox") {
|
||||
print("All chatboxes are currently busy. Try queuing instead.");
|
||||
}
|
||||
}
|
||||
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
const chatStopCommand: Command<AppContext> = {
|
||||
name: "stop",
|
||||
description: "Stop the ChatManager",
|
||||
action: ({ context }): Result<void, CliError> => {
|
||||
if (!context.chatManager) {
|
||||
print("Error: ChatManager not initialized.");
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
const result = context.chatManager.stop();
|
||||
if (result.isOk()) {
|
||||
print("ChatManager stopped successfully.");
|
||||
} else {
|
||||
print(`Failed to stop ChatManager: ${result.error.reason}`);
|
||||
}
|
||||
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
const chatClearCommand: Command<AppContext> = {
|
||||
name: "clear",
|
||||
description: "Clear queues and buffer",
|
||||
options: new Map([
|
||||
[
|
||||
"queues",
|
||||
{
|
||||
name: "queues",
|
||||
shortName: "q",
|
||||
description: "Clear message and toast queues",
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"buffer",
|
||||
{
|
||||
name: "buffer",
|
||||
shortName: "b",
|
||||
description: "Clear received message buffer",
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
action: ({ options, context }): Result<void, CliError> => {
|
||||
if (!context.chatManager) {
|
||||
print("Error: ChatManager not initialized.");
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
const clearQueues = options.queues as boolean;
|
||||
const clearBuffer = options.buffer as boolean;
|
||||
|
||||
if (!clearQueues && !clearBuffer) {
|
||||
print("Specify --queues or --buffer (or both) to clear.");
|
||||
return Ok.EMPTY;
|
||||
}
|
||||
|
||||
const results: string[] = [];
|
||||
|
||||
if (clearQueues) {
|
||||
const result = context.chatManager.clearQueues();
|
||||
if (result.isOk()) {
|
||||
results.push("Queues cleared");
|
||||
} else {
|
||||
results.push(`Failed to clear queues: ${result.error.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (clearBuffer) {
|
||||
const result = context.chatManager.clearBuffer();
|
||||
if (result.isOk()) {
|
||||
results.push("Buffer cleared");
|
||||
} else {
|
||||
results.push(`Failed to clear buffer: ${result.error.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
results.forEach((msg) => print(msg));
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
const chatCommand: Command<AppContext> = {
|
||||
name: "chat",
|
||||
description: "Chat management commands using ChatManager",
|
||||
subcommands: new Map([
|
||||
["send", chatSendCommand],
|
||||
["send-immediate", chatSendImmediateCommand],
|
||||
["toast", chatToastCommand],
|
||||
["status", chatStatusCommand],
|
||||
["receive", chatReceiveCommand],
|
||||
["stop", chatStopCommand],
|
||||
["clear", chatClearCommand],
|
||||
]),
|
||||
};
|
||||
|
||||
// 3. Define root command
|
||||
const rootCommand: Command<AppContext> = {
|
||||
name: "calculator",
|
||||
description: "A feature-rich calculator program",
|
||||
description: "A feature-rich calculator and chat management program",
|
||||
options: new Map([
|
||||
[
|
||||
"debug",
|
||||
@@ -172,6 +511,7 @@ const rootCommand: Command<AppContext> = {
|
||||
["math", mathCommand],
|
||||
["greet", greetCommand],
|
||||
["config", configCommand],
|
||||
["chat", chatCommand],
|
||||
]),
|
||||
action: ({ options, context }): Result<void, CliError> => {
|
||||
// Update debug mode from command line option
|
||||
@@ -183,20 +523,58 @@ const rootCommand: Command<AppContext> = {
|
||||
|
||||
print(`Welcome to ${context.appName}!`);
|
||||
print("Use --help to see available commands");
|
||||
|
||||
if (context.chatManager) {
|
||||
print("ChatManager initialized and ready!");
|
||||
} else {
|
||||
print("Note: No chatbox peripherals found - chat commands unavailable");
|
||||
}
|
||||
|
||||
return Ok.EMPTY;
|
||||
},
|
||||
};
|
||||
|
||||
// 4. Create global context instance
|
||||
// 4. Initialize ChatManager if chatbox peripherals are available
|
||||
function initializeChatManager(): ChatManager | undefined {
|
||||
// Find all available chatbox peripherals
|
||||
const peripheralNames = peripheral.getNames();
|
||||
const chatboxPeripherals: ChatBoxPeripheral[] = [];
|
||||
|
||||
for (const name of peripheralNames) {
|
||||
const peripheralType = peripheral.getType(name);
|
||||
if (peripheralType[0] === "chatBox") {
|
||||
const chatbox = peripheral.wrap(name) as ChatBoxPeripheral;
|
||||
chatboxPeripherals.push(chatbox);
|
||||
}
|
||||
}
|
||||
|
||||
if (chatboxPeripherals.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chatManager = new ChatManager(chatboxPeripherals);
|
||||
|
||||
// Start ChatManager in async mode so it doesn't block the CLI
|
||||
const runResult = chatManager.runAsync();
|
||||
if (runResult.isErr()) {
|
||||
print(`Warning: Failed to start ChatManager: ${runResult.error.reason}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return chatManager;
|
||||
}
|
||||
|
||||
// 5. Create global context instance
|
||||
const appContext: AppContext = {
|
||||
appName: "MyAwesomeCalculator",
|
||||
appName: "MyAwesome Calculator & Chat Manager",
|
||||
debugMode: false,
|
||||
log: (message) => {
|
||||
print(`[LOG] ${message}`);
|
||||
},
|
||||
chatManager: initializeChatManager(),
|
||||
};
|
||||
|
||||
// 5. Create and export CLI handler
|
||||
// 6. Create and export CLI handler
|
||||
const cli = createCli(rootCommand, { globalContext: appContext });
|
||||
const args = [...$vararg];
|
||||
cli(args);
|
||||
@@ -215,11 +593,24 @@ cli(['greet', '-n', 'World', '-t', '3']); // Output: Hello, World! (3 times)
|
||||
cli(['config', 'show']); // Shows current config
|
||||
cli(['config', 'set', 'theme', 'dark']); // Sets config
|
||||
|
||||
// Chat management (requires chatbox peripherals)
|
||||
cli(['chat', 'status']); // Shows ChatManager status
|
||||
cli(['chat', 'send', 'Hello World!']); // Sends global message (queued)
|
||||
cli(['chat', 'send', 'Hi there!', '--player', 'Steve']); // Private message (queued)
|
||||
cli(['chat', 'send-immediate', 'Urgent!', '--player', 'Admin']); // Immediate send
|
||||
cli(['chat', 'toast', 'Steve', 'Alert', 'Server restart in 5 minutes']); // Toast notification
|
||||
cli(['chat', 'receive', '--count', '5']); // Check for received messages
|
||||
cli(['chat', 'clear', '--queues']); // Clear pending queues
|
||||
cli(['chat', 'clear', '--buffer']); // Clear received buffer
|
||||
cli(['chat', 'stop']); // Stop ChatManager
|
||||
|
||||
// Help examples
|
||||
cli(['--help']); // Shows root help
|
||||
cli(['math', '--help']); // Shows math command help
|
||||
cli(['config', 'set', '--help']); // Shows config set help
|
||||
cli(['chat', '--help']); // Shows chat command help
|
||||
cli(['chat', 'send', '--help']); // Shows chat send help
|
||||
|
||||
// Debug mode
|
||||
cli(['--debug', 'math', 'add', '1', '2']); // Enables debug logging
|
||||
cli(['--debug', 'chat', 'status']); // Debug mode with chat status
|
||||
*/
|
||||
|
||||
637
src/lib/ChatManager.ts
Normal file
637
src/lib/ChatManager.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
import { Queue } from "./datatype/Queue";
|
||||
import { ChatBoxEvent, pullEventAs } from "./event";
|
||||
import { Result, Ok, Err } from "./thirdparty/ts-result-es";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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 */
|
||||
username: string;
|
||||
/** Title of the toast notification */
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
try {
|
||||
// Set timer to mark chatbox as idle after 1 second cooldown
|
||||
const timerId = os.startTimer(1);
|
||||
|
||||
// Start a coroutine to wait for the timer and mark chatbox as idle
|
||||
coroutine.resume(
|
||||
coroutine.create(() => {
|
||||
while (true) {
|
||||
const [_eventName, id] = os.pullEvent("timer");
|
||||
if (id === timerId) {
|
||||
this.idleChatboxes[chatboxIndex] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return new Ok(undefined);
|
||||
} catch (error) {
|
||||
// Revert chatbox state if timer setup fails
|
||||
this.idleChatboxes[chatboxIndex] = true;
|
||||
return new Err({
|
||||
kind: "ChatManager",
|
||||
reason: `Failed to set chatbox timer: ${String(error)}`,
|
||||
chatboxIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
message.message,
|
||||
message.targetPlayer,
|
||||
message.prefix,
|
||||
message.brackets,
|
||||
message.bracketColor,
|
||||
message.range,
|
||||
message.utf8Support,
|
||||
);
|
||||
} else {
|
||||
// Handle MinecraftTextComponent for private message
|
||||
[success, errorMsg] = chatbox.sendFormattedMessageToPlayer(
|
||||
JSON.stringify(message.message),
|
||||
message.targetPlayer,
|
||||
message.prefix,
|
||||
message.brackets,
|
||||
message.bracketColor,
|
||||
message.range,
|
||||
message.utf8Support,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Send global message
|
||||
if (typeof message.message === "string") {
|
||||
[success, errorMsg] = chatbox.sendMessage(
|
||||
message.message,
|
||||
message.prefix,
|
||||
message.brackets,
|
||||
message.bracketColor,
|
||||
message.range,
|
||||
message.utf8Support,
|
||||
);
|
||||
} else {
|
||||
// Handle MinecraftTextComponent for global message
|
||||
[success, errorMsg] = chatbox.sendFormattedMessage(
|
||||
JSON.stringify(message.message),
|
||||
message.prefix,
|
||||
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(
|
||||
toast.message,
|
||||
toast.title,
|
||||
toast.username,
|
||||
toast.prefix,
|
||||
toast.brackets,
|
||||
toast.bracketColor,
|
||||
toast.range,
|
||||
toast.utf8Support,
|
||||
);
|
||||
} else {
|
||||
// Handle MinecraftTextComponent for toast
|
||||
const messageJson =
|
||||
typeof toast.message === "string"
|
||||
? toast.message
|
||||
: JSON.stringify(toast.message);
|
||||
const titleJson =
|
||||
typeof toast.title === "string"
|
||||
? toast.title
|
||||
: JSON.stringify(toast.title);
|
||||
|
||||
[success, errorMsg] = chatbox.sendFormattedToastToPlayer(
|
||||
messageJson,
|
||||
titleJson,
|
||||
toast.username,
|
||||
toast.prefix,
|
||||
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 (note: event name might be "chat" based on event.ts)
|
||||
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<void, ChatManagerError> {
|
||||
if (this.isRunning) {
|
||||
return new Err({
|
||||
kind: "ChatManager",
|
||||
reason: "ChatManager is already running",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.isRunning = true;
|
||||
|
||||
// Start the run method in a separate coroutine
|
||||
coroutine.resume(
|
||||
coroutine.create(() => {
|
||||
const result = this.run();
|
||||
if (result.isErr()) {
|
||||
print(`ChatManager async error: ${result.error.reason}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return new Ok(undefined);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user