Compare commits

...

4 Commits

Author SHA1 Message Date
SikongJueluo
82a9fec46d refactor(autocraft): improve peripheral initialization with retry logic 2025-11-21 14:56:56 +08:00
SikongJueluo
94f0de4c90 docs: update README and ccStructLog documentation 2025-11-21 14:56:25 +08:00
SikongJueluo
cf7ddefc2e refactor(logging): migrate from CCLog to structured Logger 2025-11-21 14:23:10 +08:00
SikongJueluo
3287661318 refactor(logging): restructure exports and consolidate processors 2025-11-21 14:22:46 +08:00
9 changed files with 993 additions and 1108 deletions

View File

@@ -5,6 +5,7 @@ A collection of advanced utilities and libraries for Minecraft ComputerCraft, wr
## Features ## Features
### 1. Access Control System ### 1. Access Control System
A comprehensive system for managing player access to a specific area. It uses a `playerDetector` to monitor for players in range and a `chatBox` to interact with them and administrators. A comprehensive system for managing player access to a specific area. It uses a `playerDetector` to monitor for players in range and a `chatBox` to interact with them and administrators.
- **Player Detection:** Monitors a configurable range for players. - **Player Detection:** Monitors a configurable range for players.
@@ -15,6 +16,7 @@ A comprehensive system for managing player access to a specific area. It uses a
- **Logging:** Detailed logging of events, viewable with the included `logviewer` program. - **Logging:** Detailed logging of events, viewable with the included `logviewer` program.
### 2. AutoCraft System ### 2. AutoCraft System
An automated crafting solution designed to work with the Create mod's packaged recipes. An automated crafting solution designed to work with the Create mod's packaged recipes.
- **Automated Crafting:** Detects cardboard packages in a chest and automatically crafts the recipes they contain. - **Automated Crafting:** Detects cardboard packages in a chest and automatically crafts the recipes they contain.
@@ -22,6 +24,7 @@ An automated crafting solution designed to work with the Create mod's packaged r
- **Inventory Management:** Manages pulling ingredients from a source inventory and pushing crafted items to a destination. - **Inventory Management:** Manages pulling ingredients from a source inventory and pushing crafted items to a destination.
### 3. ccTUI Framework ### 3. ccTUI Framework
A declarative, reactive TUI (Terminal User Interface) framework inspired by [SolidJS](https://www.solidjs.com/) for building complex and interactive interfaces in ComputerCraft. A declarative, reactive TUI (Terminal User Interface) framework inspired by [SolidJS](https://www.solidjs.com/) for building complex and interactive interfaces in ComputerCraft.
- **Declarative Syntax:** Build UIs with simple, composable functions like `div`, `label`, `button`, and `input`. - **Declarative Syntax:** Build UIs with simple, composable functions like `div`, `label`, `button`, and `input`.
@@ -31,6 +34,7 @@ A declarative, reactive TUI (Terminal User Interface) framework inspired by [Sol
- **Component-Based:** Structure your UI into reusable components. See `src/tuiExample/main.ts` for a demo. - **Component-Based:** Structure your UI into reusable components. See `src/tuiExample/main.ts` for a demo.
### 4. ccCLI Framework ### 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. 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. - **Declarative API:** Define commands, arguments, and options using a simple, object-based structure.
@@ -40,6 +44,7 @@ A lightweight, functional-style framework for building command-line interfaces (
- **Type-Safe:** Built with TypeScript for robust development. - **Type-Safe:** Built with TypeScript for robust development.
### 5. Core Libraries ### 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. - **`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.
- **`ccStructLog`:** A modern, structured logging library inspired by Python's `structlog`. It provides a flexible, extensible framework based on processors, renderers, and streams, designed for CC:Tweaked. See the [ccStructLog Documentation](./docs/ccStructLog.md) for more details. - **`ccStructLog`:** A modern, structured logging library inspired by Python's `structlog`. It provides a flexible, extensible framework based on processors, renderers, and streams, designed for CC:Tweaked. See the [ccStructLog Documentation](./docs/ccStructLog.md) for more details.
- **`PeripheralManager`:** A utility for easily finding and requiring peripherals by name or type. - **`PeripheralManager`:** A utility for easily finding and requiring peripherals by name or type.
@@ -54,6 +59,7 @@ A lightweight, functional-style framework for building command-line interfaces (
## Setup & Installation ## Setup & Installation
1. **Clone the repository:** 1. **Clone the repository:**
```sh ```sh
git clone <repository-url> git clone <repository-url>
cd cc-utils cd cc-utils
@@ -69,6 +75,7 @@ A lightweight, functional-style framework for building command-line interfaces (
This project uses `just` to manage build tasks. The compiled Lua files will be placed in the `build/` directory. This project uses `just` to manage build tasks. The compiled Lua files will be placed in the `build/` directory.
- **Build all modules:** - **Build all modules:**
```sh ```sh
just build just build
``` ```
@@ -89,7 +96,7 @@ To deploy the built programs to your in-game computer, you need to configure the
```justfile ```justfile
# Example for Linux # Example for Linux
sync-path := "/home/user/.local/share/craftos-pc/computer/0/user/" sync-path := "/home/user/.local/share/craftos-pc/computer/0/user/"
# Example for Windows # Example for Windows
# sync-path := "/cygdrive/c/Users/YourUser/AppData/Roaming/CraftOS-PC/computer/0/user/" # sync-path := "/cygdrive/c/Users/YourUser/AppData/Roaming/CraftOS-PC/computer/0/user/"
``` ```
@@ -105,44 +112,30 @@ To deploy the built programs to your in-game computer, you need to configure the
### Access Control ### Access Control
- **Start the system:** - **Start the system:**
```sh ```sh
accesscontrol start accesscontrol start
``` ```
- **Open the configuration TUI:** - **Open the configuration TUI:**
```sh ```sh
accesscontrol config accesscontrol config
``` ```
Alternatively, press `c` while the main program is running.
- **View logs:** Alternatively, press `c` while the main program is running.
```sh
logviewer accesscontrol.log
```
- **Admin Commands (in-game chat):** - **Admin Commands (in-game chat):**
``` ```
@AC /help @AC help
@AC /add user Notch @AC add user Notch
@AC /list @AC list
``` ```
### AutoCraft ### AutoCraft
The autocraft program runs in the background. Simply run it on a turtle with the correct peripheral setup (see `src/autocraft/main.ts`). It will automatically process packages placed in the designated chest. The autocraft program runs in the background. Simply run it on a turtle with the correct peripheral setup (see `src/autocraft/main.ts`). It will automatically process packages placed in the designated chest.
```sh
autocraft
```
### TUI Example
Run the example program to see a demonstration of the `ccTUI` framework.
```sh
tuiExample
```
## Development ## Development
- **Lint and format the code:** - **Lint and format the code:**

View File

@@ -11,16 +11,37 @@ A modern, structured logging library for CC:Tweaked, inspired by Python's struct
## Quick Start ## Quick Start
```typescript The easiest way to get started is to create a `Logger` instance and configure it with processors, a renderer, and streams.
import { createDevLogger } from "@/lib/ccStructLog";
// Create a development logger Here's a simple example of a logger that prints colored, human-readable messages to the console:
const logger = createDevLogger();
```typescript
import {
Logger,
LogLevel,
processor,
textRenderer,
ConsoleStream,
} from "@/lib/ccStructLog";
// Create a logger
const logger = new Logger({
processors: [
processor.addTimestamp({ format: "%T" }), // Add HH:MM:SS timestamp
processor.filterByLevel(LogLevel.Info), // Log Info and higher
processor.addSource("MyApp"),
],
renderer: textRenderer,
streams: [new ConsoleStream()],
});
// Log messages with context // Log messages with context
logger.info("Server started", { port: 8080, version: "1.0.0" }); logger.info("Server started", { port: 8080, version: "1.0.0" });
logger.warn("Low disk space", { available: 1024, threshold: 2048 }); logger.warn("Low disk space", { available: 1024, threshold: 2048 });
logger.error("Connection failed", { host: "example.com", retries: 3 }); logger.error("Connection failed", { host: "example.com", retries: 3 });
// This debug message will be filtered out by `filterByLevel`
logger.debug("This is a debug message.");
``` ```
## Core Concepts ## Core Concepts
@@ -46,48 +67,69 @@ export enum LogLevel {
4. **Render**: The final event is converted to a string by a renderer (e.g., `textRenderer`, `jsonRenderer`). 4. **Render**: The final event is converted to a string by a renderer (e.g., `textRenderer`, `jsonRenderer`).
5. **Output**: The string is sent to one or more streams (e.g., console, file). 5. **Output**: The string is sent to one or more streams (e.g., console, file).
## Pre-configured Loggers ## Common Configurations
### Development Logger ### Development Logger
Optimized for development and debugging with human-readable console output. A typical development logger is configured for human-readable console output with timestamps and colors.
```typescript ```typescript
import { createDevLogger, LogLevel } from "@/lib/ccStructLog"; import {
Logger,
processor,
textRenderer,
ConsoleStream,
} from "@/lib/ccStructLog";
const logger = createDevLogger({ const devLogger = new Logger({
source: "MyApp", processors: [
includeComputerId: true, processor.addTimestamp({ format: "%F %T" }), // YYYY-MM-DD HH:MM:SS
processor.addSource("DevApp"),
processor.addComputerId(),
],
renderer: textRenderer,
streams: [new ConsoleStream()],
}); });
logger.debug("This is a debug message."); devLogger.debug("This is a debug message.", { user: "dev" });
``` ```
### Production Logger ### Production Logger
Optimized for production with JSON-formatted file output and daily rotation. A production logger is often configured to write machine-readable JSON logs to a file with daily rotation.
```typescript
import { createProdLogger, DAY } from "@/lib/ccStructLog";
const logger = createProdLogger("app.log", {
source: "MyApp",
rotationInterval: DAY, // Rotate daily
includeConsole: false, // Don't log to console
});
logger.info("Application is running in production.");
```
## Custom Configuration
You can create a logger with a completely custom setup.
```typescript ```typescript
import { import {
Logger, Logger,
LogLevel, LogLevel,
addFullTimestamp, processor,
addComputerId, jsonRenderer,
addSource, FileStream,
DAY,
} from "@/lib/ccStructLog";
const prodLogger = new Logger({
processors: [
processor.addTimestamp(), // Default format is %F %T
processor.filterByLevel(LogLevel.Info),
processor.addSource("ProdApp"),
processor.addComputerId(),
],
renderer: jsonRenderer,
streams: [
new FileStream("app.log", DAY), // Rotate daily
],
});
prodLogger.info("Application is running in production.");
```
## Custom Configuration
You can create a logger with any combination of processors, renderers, and streams.
```typescript
import {
Logger,
processor,
jsonRenderer, jsonRenderer,
FileStream, FileStream,
ConsoleStream, ConsoleStream,
@@ -96,9 +138,9 @@ import {
const logger = new Logger({ const logger = new Logger({
processors: [ processors: [
addFullTimestamp(), processor.addTimestamp(),
addComputerId(), processor.addComputerId(),
addSource("MyApplication"), processor.addSource("MyApplication"),
], ],
renderer: jsonRenderer, renderer: jsonRenderer,
streams: [ streams: [
@@ -112,31 +154,40 @@ logger.info("Custom logger reporting for duty.", { user: "admin" });
## Processors ## Processors
Processors are functions that modify, enrich, or filter log events before they are rendered. Processors are functions that modify, enrich, or filter log events before they are rendered. They are all available under the `processor` namespace.
### Built-in Processors ### Built-in Processors
```typescript ```typescript
import { import { Logger, LogLevel, processor } from "@/lib/ccStructLog";
addTimestamp, // Add structured timestamp
addFormattedTimestamp, // Add HH:MM:SS string
addFullTimestamp, // Add YYYY-MM-DD HH:MM:SS string
filterByLevel, // Filter by minimum level
filterBy, // Filter based on a custom predicate
addSource, // Add source/logger name
addComputerId, // Add computer ID
addComputerLabel, // Add computer label
addStaticFields, // Add static fields to all events
transformField, // Transform a specific field's value
removeFields, // Remove sensitive fields
} from "@/lib/ccStructLog";
// Usage example // Usage example
const logger = new Logger({ const logger = new Logger({
processors: [ processors: [
addTimestamp(), // Adds a timestamp. Format is compatible with os.date().
addSource("MyApp"), // Default: "%F %T" (e.g., "2023-10-27 15:30:00")
filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal processor.addTimestamp({ format: "%T" }), // e.g., "15:30:00"
removeFields(["password", "token"]),
// Filter by minimum level
processor.filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal
// Filter based on a custom predicate
processor.filterBy((event) => event.get("user") === "admin"),
// Add source/logger name
processor.addSource("MyApp"),
// Add computer ID or label
processor.addComputerId(),
processor.addComputerLabel(),
// Add static fields to all events
processor.addStaticFields({ env: "production", version: "1.2.3" }),
// Transform a specific field's value
processor.transformField("user_id", (id) => `user_${id}`),
// Remove sensitive fields
processor.removeFields(["password", "token"]),
], ],
// ... other config // ... other config
}); });
@@ -172,10 +223,10 @@ Renderers convert the final `LogEvent` object into a string.
import { textRenderer, jsonRenderer } from "@/lib/ccStructLog"; import { textRenderer, jsonRenderer } from "@/lib/ccStructLog";
// textRenderer: Human-readable, colored output for the console. // textRenderer: Human-readable, colored output for the console.
// Example: 15:30:45 [INFO] Message key=value // Example: [15:30:45] [INFO] Message key=value
// jsonRenderer: Machine-readable JSON output. // jsonRenderer: Machine-readable JSON output.
// Example: {"level":"info","message":"Message","key":"value","timestamp":"..."} // Example: {"level":2,"message":"Message","key":"value","timestamp":"15:30:45"}
``` ```
## Streams ## Streams
@@ -185,12 +236,14 @@ Streams handle the final output destination. You can use multiple streams to sen
### Built-in Streams ### Built-in Streams
```typescript ```typescript
import { import {
ConsoleStream, // Output to CC:Tweaked terminal with colors ConsoleStream,
FileStream, // Output to file with rotation support FileStream,
BufferStream, // Store in an in-memory buffer BufferStream,
NullStream, // Discard all output NullStream,
ConditionalStream,
LogLevel,
DAY,
} from "@/lib/ccStructLog"; } from "@/lib/ccStructLog";
import { ConditionalStream } from "@/lib/ccStructLog/streams"; // Note direct import
// File stream with daily rotation // File stream with daily rotation
const fileStream = new FileStream("app.log", DAY); const fileStream = new FileStream("app.log", DAY);
@@ -207,7 +260,7 @@ const errorStream = new ConditionalStream(
## File Rotation ## File Rotation
`FileStream` supports automatic file rotation based on time intervals. `FileStream` supports automatic file rotation based on time intervals. The rotation interval is specified in seconds as the second argument to the constructor.
```typescript ```typescript
import { FileStream, HOUR, DAY, WEEK } from "@/lib/ccStructLog"; import { FileStream, HOUR, DAY, WEEK } from "@/lib/ccStructLog";
@@ -221,7 +274,7 @@ const dailyLog = new FileStream("app_daily.log", DAY);
// Rotate weekly // Rotate weekly
const weeklyLog = new FileStream("app_weekly.log", WEEK); const weeklyLog = new FileStream("app_weekly.log", WEEK);
// No rotation // No rotation (pass 0 or undefined)
const permanentLog = new FileStream("permanent.log", 0); const permanentLog = new FileStream("permanent.log", 0);
``` ```
@@ -243,15 +296,22 @@ const permanentLog = new FileStream("permanent.log", 0);
- `error`: Errors that affect a single operation but not the whole app. - `error`: Errors that affect a single operation but not the whole app.
- `fatal`: Critical errors that require the application to shut down. - `fatal`: Critical errors that require the application to shut down.
3. **Use a `source`**: Identify which component generated the log. 3. **Use a `source`**: Identify which component generated the log using `processor.addSource`.
```typescript ```typescript
const logger = createDevLogger({ source: "UserService" }); import { Logger, processor } from "@/lib/ccStructLog";
const logger = new Logger({
processors: [processor.addSource("UserService")],
// ...
});
``` ```
4. **Sanitize Sensitive Data**: Use a processor to remove passwords, API keys, etc. 4. **Sanitize Sensitive Data**: Use a processor to remove passwords, API keys, etc.
```typescript ```typescript
import { Logger, processor } from "@/lib/ccStructLog";
const secureLogger = new Logger({ const secureLogger = new Logger({
processors: [ removeFields(["password", "token"]) ], processors: [ processor.removeFields(["password", "token"]) ],
//... //...
}); });
``` ```

View File

@@ -1,4 +1,3 @@
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";
@@ -7,14 +6,31 @@ import { ReadWriteLock } from "@/lib/mutex/ReadWriteLock";
import { ChatManager } from "@/lib/ChatManager"; import { ChatManager } from "@/lib/ChatManager";
import { gTimerManager } from "@/lib/TimerManager"; import { gTimerManager } from "@/lib/TimerManager";
import { KeyEvent, pullEventAs } from "@/lib/event"; import { KeyEvent, pullEventAs } from "@/lib/event";
import {
ConditionalStream,
ConsoleStream,
DAY,
FileStream,
Logger,
LogLevel,
processor,
textRenderer,
} from "@/lib/ccStructLog";
const args = [...$vararg]; const args = [...$vararg];
// Init Log // Init Log
const logger = new CCLog("accesscontrol.log", { let isOnConsoleStream = true;
printTerminal: true, const logger = new Logger({
logInterval: DAY, processors: [
outputMinLevel: LogLevel.Info, processor.filterByLevel(LogLevel.Info),
processor.addTimestamp(),
],
renderer: textRenderer,
streams: [
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
new FileStream("accesscontrol.log", DAY),
],
}); });
// Load Config // Load Config
@@ -26,7 +42,7 @@ logger.debug(textutils.serialise(config, { allow_repetitions: true }));
// Peripheral // Peripheral
const playerDetector = peripheral.find( const playerDetector = peripheral.find(
"playerDetector", "playerDetector",
)[0] as PlayerDetectorPeripheral; )[0] as PlayerDetectorPeripheral;
const chatBox = peripheral.find("chatBox")[0] as ChatBoxPeripheral; const chatBox = peripheral.find("chatBox")[0] as ChatBoxPeripheral;
const chatManager: ChatManager = new ChatManager([chatBox]); const chatManager: ChatManager = new ChatManager([chatBox]);
@@ -37,410 +53,419 @@ let gWatchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
let gIsRunning = true; let gIsRunning = true;
interface ParseParams { interface ParseParams {
playerName?: string; playerName?: string;
groupName?: string; groupName?: string;
info?: PlayerInfo; info?: PlayerInfo;
} }
function reloadConfig() { function reloadConfig() {
let releaser = configLock.tryAcquireWrite(); let releaser = configLock.tryAcquireWrite();
while (releaser === undefined) { while (releaser === undefined) {
sleep(1); sleep(1);
releaser = configLock.tryAcquireWrite(); releaser = configLock.tryAcquireWrite();
} }
config = loadConfig(configFilepath)!; config = loadConfig(configFilepath)!;
gInRangePlayers = []; gInRangePlayers = [];
gWatchPlayersInfo = []; gWatchPlayersInfo = [];
releaser.release(); releaser.release();
logger.info("Reload config successfully!"); logger.info("Reload config successfully!");
} }
function safeParseTextComponent( function safeParseTextComponent(
component: MinecraftTextComponent, component: MinecraftTextComponent,
params?: ParseParams, params?: ParseParams,
): MinecraftTextComponent { ): MinecraftTextComponent {
const newComponent = deepCopy(component); const newComponent = deepCopy(component);
if (newComponent.text == undefined) { if (newComponent.text == undefined) {
newComponent.text = "Wrong text, please contanct with admin"; newComponent.text = "Wrong text, please contanct with admin";
} else if (newComponent.text.includes("%")) { } else if (newComponent.text.includes("%")) {
newComponent.text = newComponent.text.replace( newComponent.text = newComponent.text.replace(
"%playerName%", "%playerName%",
params?.playerName ?? "UnknowPlayer", params?.playerName ?? "UnknowPlayer",
); );
newComponent.text = newComponent.text.replace( newComponent.text = newComponent.text.replace(
"%groupName%", "%groupName%",
params?.groupName ?? "UnknowGroup", params?.groupName ?? "UnknowGroup",
); );
newComponent.text = newComponent.text.replace( newComponent.text = newComponent.text.replace(
"%playerPosX%", "%playerPosX%",
params?.info?.x.toString() ?? "UnknowPosX", params?.info?.x.toString() ?? "UnknowPosX",
); );
newComponent.text = newComponent.text.replace( newComponent.text = newComponent.text.replace(
"%playerPosY%", "%playerPosY%",
params?.info?.y.toString() ?? "UnknowPosY", params?.info?.y.toString() ?? "UnknowPosY",
); );
newComponent.text = newComponent.text.replace( newComponent.text = newComponent.text.replace(
"%playerPosZ%", "%playerPosZ%",
params?.info?.z.toString() ?? "UnknowPosZ", params?.info?.z.toString() ?? "UnknowPosZ",
); );
} }
return newComponent; return newComponent;
} }
function sendMessage( function sendMessage(
toastConfig: ToastConfig, toastConfig: ToastConfig,
targetPlayer: string, targetPlayer: string,
params: ParseParams, params: ParseParams,
) { ) {
let releaser = configLock.tryAcquireRead(); let releaser = configLock.tryAcquireRead();
while (releaser === undefined) { while (releaser === undefined) {
sleep(0.1); sleep(0.1);
releaser = configLock.tryAcquireRead(); releaser = configLock.tryAcquireRead();
} }
chatManager.sendMessage({ chatManager.sendMessage({
message: safeParseTextComponent( message: safeParseTextComponent(
toastConfig.msg ?? config.welcomeToastConfig.msg, toastConfig.msg ?? config.welcomeToastConfig.msg,
params, params,
), ),
prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix, prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets, brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
bracketColor: bracketColor:
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor, toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
targetPlayer: targetPlayer, targetPlayer: targetPlayer,
utf8Support: true, utf8Support: true,
}); });
releaser.release(); releaser.release();
} }
function sendToast( function sendToast(
toastConfig: ToastConfig, toastConfig: ToastConfig,
targetPlayer: string, targetPlayer: string,
params: ParseParams, params: ParseParams,
) { ) {
let releaser = configLock.tryAcquireRead(); let releaser = configLock.tryAcquireRead();
while (releaser === undefined) { while (releaser === undefined) {
sleep(0.1); sleep(0.1);
releaser = configLock.tryAcquireRead(); releaser = configLock.tryAcquireRead();
} }
chatManager.sendToast({ chatManager.sendToast({
message: safeParseTextComponent( message: safeParseTextComponent(
toastConfig.msg ?? config.welcomeToastConfig.msg, toastConfig.msg ?? config.welcomeToastConfig.msg,
params, params,
), ),
title: safeParseTextComponent( title: safeParseTextComponent(
toastConfig.title ?? config.welcomeToastConfig.title, toastConfig.title ?? config.welcomeToastConfig.title,
params, params,
), ),
prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix, prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets, brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
bracketColor: bracketColor:
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor, toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
targetPlayer: targetPlayer, targetPlayer: targetPlayer,
utf8Support: true, utf8Support: true,
}); });
releaser.release(); releaser.release();
} }
function sendNotice(player: string, playerInfo?: PlayerInfo) { function sendNotice(player: string, playerInfo?: PlayerInfo) {
let releaser = configLock.tryAcquireRead(); let releaser = configLock.tryAcquireRead();
while (releaser === undefined) { while (releaser === undefined) {
sleep(0.1); sleep(0.1);
releaser = configLock.tryAcquireRead(); releaser = configLock.tryAcquireRead();
} }
const onlinePlayers = playerDetector.getOnlinePlayers(); const onlinePlayers = playerDetector.getOnlinePlayers();
const noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat( const noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat(
config.usersGroups config.usersGroups
.filter((value) => value.isNotice) .filter((value) => value.isNotice)
.flatMap((value) => value.groupUsers ?? []), .flatMap((value) => value.groupUsers ?? []),
); );
logger.debug(`noticeTargetPlayers: ${noticeTargetPlayers.join(", ")}`); logger.debug(`noticeTargetPlayers: ${noticeTargetPlayers.join(", ")}`);
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, {
playerName: player, playerName: player,
info: playerInfo, info: playerInfo,
}); });
sleep(1); sleep(1);
} }
releaser.release(); releaser.release();
} }
function sendWarn(player: string) { function sendWarn(player: string) {
const warnMsg = `Not Allowed Player ${player} Break in Home `; const warnMsg = `Not Allowed Player ${player} Break in Home `;
logger.warn(warnMsg); logger.warn(warnMsg);
let releaser = configLock.tryAcquireRead();
while (releaser === undefined) {
sleep(0.1);
releaser = configLock.tryAcquireRead();
}
sendToast(config.warnToastConfig, player, { playerName: player });
chatManager.sendMessage({
message: safeParseTextComponent(config.warnToastConfig.msg, {
playerName: player,
}),
targetPlayer: player,
prefix: "AccessControl",
brackets: "[]",
utf8Support: true,
});
releaser.release();
}
function watchLoop() {
while (gIsRunning) {
const releaser = configLock.tryAcquireRead();
if (releaser === undefined) {
os.sleep(1);
continue;
}
const watchPlayerNames = gWatchPlayersInfo.flatMap((value) => value.name);
logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
for (const player of gWatchPlayersInfo) {
const playerInfo = playerDetector.getPlayerPos(player.name);
if (gInRangePlayers.includes(player.name)) {
// Notice
if (player.hasNoticeTimes < config.noticeTimes) {
sendNotice(player.name, playerInfo);
player.hasNoticeTimes += 1;
}
// Warn
if (config.isWarn) sendWarn(player.name);
// Record
logger.warn(
`Stranger ${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
} else {
// Get rid of player from list
gWatchPlayersInfo = gWatchPlayersInfo.filter(
(value) => value.name != player.name,
);
logger.info(
`Stranger ${player.name} has left the range at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
}
os.sleep(1);
}
releaser.release();
os.sleep(config.watchInterval);
}
}
function mainLoop() {
while (gIsRunning) {
const releaser = configLock.tryAcquireRead();
if (releaser === undefined) {
os.sleep(0.1);
continue;
}
const players = playerDetector.getPlayersInRange(config.detectRange);
const playersList = "[ " + players.join(",") + " ]";
logger.debug(`Detected ${players.length} players: ${playersList}`);
for (const player of players) {
if (gInRangePlayers.includes(player)) continue;
// Get player Info
const playerInfo = playerDetector.getPlayerPos(player);
if (config.adminGroupConfig.groupUsers.includes(player)) {
logger.info(
`Admin ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
if (config.adminGroupConfig.isWelcome)
sendMessage(config.welcomeToastConfig, player, {
playerName: player,
groupName: "Admin",
info: playerInfo,
});
continue;
}
// New player appear
let groupConfig: UserGroupConfig = {
groupName: "Unfamiliar",
groupUsers: [],
isAllowed: false,
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;
groupConfig = userGroupConfig;
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 (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,
{ name: player, hasNoticeTimes: 0 },
];
}
gInRangePlayers = players;
releaser.release();
os.sleep(config.detectInterval);
}
}
function keyboardLoop() {
while (gIsRunning) {
const event = pullEventAs(KeyEvent, "key");
if (event === undefined) continue;
if (event.key === keys.c) {
logger.info("Launching Access Control TUI...");
try {
logger.setInTerminal(false);
launchAccessControlTUI();
logger.info("TUI closed, resuming normal operation");
} catch (error) {
logger.error(`TUI error: ${textutils.serialise(error as object)}`);
} finally {
logger.setInTerminal(true);
reloadConfig();
}
} else if (event.key === keys.r) {
reloadConfig();
}
// else if (event.key === keys.q) {
// gIsRunning = false;
// }
}
}
function cliLoop() {
let printTargetPlayer: string | undefined;
const cli = createAccessControlCli({
configFilepath: configFilepath,
reloadConfig: () => reloadConfig(),
logger: logger,
print: (msg) =>
chatManager.sendMessage({
message: msg,
targetPlayer: printTargetPlayer,
prefix: "Access Control System",
brackets: "[]",
utf8Support: true,
}),
});
while (gIsRunning) {
const result = chatManager.getReceivedMessage();
if (result.isErr()) {
sleep(0.5);
continue;
}
logger.debug(`Received message: ${result.value.message}`);
const ev = result.value;
let releaser = configLock.tryAcquireRead(); let releaser = configLock.tryAcquireRead();
while (releaser === undefined) { while (releaser === undefined) {
sleep(0.1); sleep(0.1);
releaser = configLock.tryAcquireRead(); releaser = configLock.tryAcquireRead();
} }
const isAdmin = config.adminGroupConfig.groupUsers.includes(ev.username); sendToast(config.warnToastConfig, player, { playerName: player });
chatManager.sendMessage({
message: safeParseTextComponent(config.warnToastConfig.msg, {
playerName: player,
}),
targetPlayer: player,
prefix: "AccessControl",
brackets: "[]",
utf8Support: true,
});
releaser.release(); releaser.release();
if (!isAdmin) continue; }
if (!ev.message.startsWith("@AC")) continue;
printTargetPlayer = ev.username; function watchLoop() {
logger.info( while (gIsRunning) {
`Received command "${ev.message}" from admin ${printTargetPlayer}`, const releaser = configLock.tryAcquireRead();
); if (releaser === undefined) {
os.sleep(1);
continue;
}
const commandArgs = ev.message const watchPlayerNames = gWatchPlayersInfo.flatMap(
.substring(3) (value) => value.name,
.split(" ") );
.filter((s) => s.length > 0); logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
logger.debug(`Command arguments: ${commandArgs.join(", ")}`); for (const player of gWatchPlayersInfo) {
const playerInfo = playerDetector.getPlayerPos(player.name);
if (gInRangePlayers.includes(player.name)) {
// Notice
if (player.hasNoticeTimes < config.noticeTimes) {
sendNotice(player.name, playerInfo);
player.hasNoticeTimes += 1;
}
cli(commandArgs); // Warn
printTargetPlayer = undefined; if (config.isWarn) sendWarn(player.name);
}
// Record
logger.warn(
`Stranger ${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
} else {
// Get rid of player from list
gWatchPlayersInfo = gWatchPlayersInfo.filter(
(value) => value.name != player.name,
);
logger.info(
`Stranger ${player.name} has left the range at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
}
os.sleep(1);
}
releaser.release();
os.sleep(config.watchInterval);
}
}
function mainLoop() {
while (gIsRunning) {
const releaser = configLock.tryAcquireRead();
if (releaser === undefined) {
os.sleep(0.1);
continue;
}
const players = playerDetector.getPlayersInRange(config.detectRange);
const playersList = "[ " + players.join(",") + " ]";
logger.debug(`Detected ${players.length} players: ${playersList}`);
for (const player of players) {
if (gInRangePlayers.includes(player)) continue;
// Get player Info
const playerInfo = playerDetector.getPlayerPos(player);
if (config.adminGroupConfig.groupUsers.includes(player)) {
logger.info(
`Admin ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
);
if (config.adminGroupConfig.isWelcome)
sendMessage(config.welcomeToastConfig, player, {
playerName: player,
groupName: "Admin",
info: playerInfo,
});
continue;
}
// New player appear
let groupConfig: UserGroupConfig = {
groupName: "Unfamiliar",
groupUsers: [],
isAllowed: false,
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;
groupConfig = userGroupConfig;
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 (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,
{ name: player, hasNoticeTimes: 0 },
];
}
gInRangePlayers = players;
releaser.release();
os.sleep(config.detectInterval);
}
}
function keyboardLoop() {
while (gIsRunning) {
const event = pullEventAs(KeyEvent, "key");
if (event === undefined) continue;
if (event.key === keys.c) {
logger.info("Launching Access Control TUI...");
try {
isOnConsoleStream = false;
launchAccessControlTUI();
logger.info("TUI closed, resuming normal operation");
} catch (error) {
logger.error(
`TUI error: ${textutils.serialise(error as object)}`,
);
} finally {
isOnConsoleStream = true;
reloadConfig();
}
} else if (event.key === keys.r) {
reloadConfig();
}
// else if (event.key === keys.q) {
// gIsRunning = false;
// }
}
}
function cliLoop() {
let printTargetPlayer: string | undefined;
const cli = createAccessControlCli({
configFilepath: configFilepath,
reloadConfig: () => reloadConfig(),
logger: logger,
print: (msg) =>
chatManager.sendMessage({
message: msg,
targetPlayer: printTargetPlayer,
prefix: "Access Control System",
brackets: "[]",
utf8Support: true,
}),
});
while (gIsRunning) {
const result = chatManager.getReceivedMessage();
if (result.isErr()) {
sleep(0.5);
continue;
}
logger.debug(`Received message: ${result.value.message}`);
const ev = result.value;
let releaser = configLock.tryAcquireRead();
while (releaser === undefined) {
sleep(0.1);
releaser = configLock.tryAcquireRead();
}
const isAdmin = config.adminGroupConfig.groupUsers.includes(
ev.username,
);
releaser.release();
if (!isAdmin) continue;
if (!ev.message.startsWith("@AC")) continue;
printTargetPlayer = ev.username;
logger.info(
`Received command "${ev.message}" from admin ${printTargetPlayer}`,
);
const commandArgs = ev.message
.substring(3)
.split(" ")
.filter((s) => s.length > 0);
logger.debug(`Command arguments: ${commandArgs.join(", ")}`);
cli(commandArgs);
printTargetPlayer = undefined;
}
} }
function main(args: string[]) { 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") {
const tutorial: string[] = []; const tutorial: string[] = [];
tutorial.push("Access Control System started."); tutorial.push("Access Control System started.");
tutorial.push("\tPress 'c' to open configuration TUI."); tutorial.push("\tPress 'c' to open configuration TUI.");
tutorial.push("\tPress 'r' to reload configuration."); tutorial.push("\tPress 'r' to reload configuration.");
print(tutorial.join("\n")); print(tutorial.join("\n"));
parallel.waitForAll( parallel.waitForAll(
() => mainLoop(), () => mainLoop(),
() => gTimerManager.run(), () => gTimerManager.run(),
() => cliLoop(), () => cliLoop(),
() => watchLoop(), () => watchLoop(),
() => keyboardLoop(), () => keyboardLoop(),
() => chatManager.run(), () => chatManager.run(),
); );
return; return;
} else if (args[0] == "config") { } else if (args[0] == "config") {
logger.info("Launching Access Control TUI..."); logger.info("Launching Access Control TUI...");
logger.setInTerminal(false); isOnConsoleStream = false;
try {
launchAccessControlTUI(); try {
} catch (error) { launchAccessControlTUI();
logger.error(`TUI error: ${textutils.serialise(error as object)}`); } catch (error) {
} logger.error(
return; `TUI error: ${textutils.serialise(error as object)}`,
);
}
return;
}
} }
}
print(`Usage: accesscontrol start | config`); print(`Usage: accesscontrol start | config`);
print(" start - Start the access control system with monitoring"); print(" start - Start the access control system with monitoring");
print(" config - Open configuration TUI"); print(" config - Open configuration TUI");
} }
try { try {
main(args); main(args);
} catch (error: unknown) { } catch (error: unknown) {
logger.error(textutils.serialise(error as object)); logger.error(textutils.serialise(error as object));
} finally { } finally {
logger.close(); logger.close();
} }

View File

@@ -1,215 +1,262 @@
import { import {
CraftManager, CraftManager,
CraftRecipe, CraftRecipe,
CreatePackageTag, CreatePackageTag,
} from "@/lib/CraftManager"; } from "@/lib/CraftManager";
import { CCLog, LogLevel } from "@/lib/ccLog";
import { Queue } from "@/lib/datatype/Queue"; import { Queue } from "@/lib/datatype/Queue";
import {
ConsoleStream,
Logger,
LogLevel,
processor,
textRenderer,
} from "@/lib/ccStructLog";
const logger = new CCLog("autocraft.log", { outputMinLevel: LogLevel.Info }); const logger = new Logger({
processors: [
processor.filterByLevel(LogLevel.Info),
processor.addTimestamp(),
],
renderer: textRenderer,
streams: [new ConsoleStream()],
});
const peripheralsNames = { const peripheralsNames = {
// packsInventory: "minecraft:chest_14", // packsInventory: "minecraft:chest_14",
// itemsInventory: "minecraft:chest_15", // itemsInventory: "minecraft:chest_15",
// packageExtractor: "create:packager_3", // packageExtractor: "create:packager_3",
blockReader: "bottom", blockReader: "bottom",
wiredModem: "right", wiredModem: "right",
redstone: "left", redstone: "left",
packsInventory: "minecraft:chest_1121", packsInventory: "minecraft:chest_1121",
itemsInventory: "minecraft:chest_1120", itemsInventory: "minecraft:chest_1120",
packageExtractor: "create:packager_0", packageExtractor: "create:packager_0",
}; };
const packsInventory = peripheral.wrap( let packsInventory: InventoryPeripheral;
peripheralsNames.packsInventory, let itemsInventory: InventoryPeripheral;
) as InventoryPeripheral; let packageExtractor: InventoryPeripheral;
const itemsInventory = peripheral.wrap( let blockReader: BlockReaderPeripheral;
peripheralsNames.itemsInventory, let wiredModem: WiredModemPeripheral;
) as InventoryPeripheral; let turtleLocalName: string;
const packageExtractor = peripheral.wrap(
peripheralsNames.packageExtractor,
) as InventoryPeripheral;
const blockReader = peripheral.wrap(
peripheralsNames.blockReader,
) as BlockReaderPeripheral;
const wiredModem = peripheral.wrap(
peripheralsNames.wiredModem,
) as WiredModemPeripheral;
const turtleLocalName = wiredModem.getNameLocal();
enum State { enum State {
IDLE, IDLE,
READ_RECIPE, READ_RECIPE,
CRAFT_OUTPUT, CRAFT_OUTPUT,
} }
function main() { function main() {
const craftManager = new CraftManager(turtleLocalName, itemsInventory); while (true) {
const recipesQueue = new Queue<CraftRecipe>(); try {
const recipesWaitingMap = new Map<number, CraftRecipe[] | CraftRecipe>(); packsInventory = peripheral.wrap(
let currentState = State.IDLE; peripheralsNames.packsInventory,
let nextState = State.IDLE; ) as InventoryPeripheral;
let hasPackage = redstone.getInput(peripheralsNames.redstone); itemsInventory = peripheral.wrap(
while (hasPackage) { peripheralsNames.itemsInventory,
hasPackage = redstone.getInput(peripheralsNames.redstone); ) as InventoryPeripheral;
logger.warn("redstone activated when init, please clear inventory"); packageExtractor = peripheral.wrap(
sleep(1); peripheralsNames.packageExtractor,
} ) as InventoryPeripheral;
blockReader = peripheral.wrap(
peripheralsNames.blockReader,
) as BlockReaderPeripheral;
wiredModem = peripheral.wrap(
peripheralsNames.wiredModem,
) as WiredModemPeripheral;
turtleLocalName = wiredModem.getNameLocal();
logger.info("AutoCraft init finished..."); logger.info("Peripheral initialization complete...");
while (true) { break;
// Switch state } catch (error) {
switch (currentState) { logger.warn(
case State.IDLE: { `Peripheral initialization failed for ${String(error)}, try again...`,
nextState = hasPackage ? State.READ_RECIPE : State.IDLE; );
break; sleep(1);
} }
case State.READ_RECIPE: {
nextState = hasPackage ? State.READ_RECIPE : State.CRAFT_OUTPUT;
break;
}
case State.CRAFT_OUTPUT: {
nextState =
recipesQueue.size() > 0
? State.CRAFT_OUTPUT
: hasPackage
? State.READ_RECIPE
: State.IDLE;
break;
}
default: {
logger.error(`Unknown state`);
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
break;
}
} }
// State logic const craftManager = new CraftManager(turtleLocalName, itemsInventory);
switch (currentState) { const recipesQueue = new Queue<CraftRecipe>();
case State.IDLE: { const recipesWaitingMap = new Map<number, CraftRecipe[] | CraftRecipe>();
if (!hasPackage) os.pullEvent("redstone");
let currentState = State.IDLE;
let nextState = State.IDLE;
let hasPackage = redstone.getInput(peripheralsNames.redstone);
while (hasPackage) {
hasPackage = redstone.getInput(peripheralsNames.redstone); hasPackage = redstone.getInput(peripheralsNames.redstone);
break; logger.warn("redstone activated when init, please clear inventory");
}
case State.READ_RECIPE: {
logger.info(`Package detected`);
const packagesInfoRecord = packsInventory.list();
for (const key in packagesInfoRecord) {
const slotNum = parseInt(key);
packsInventory.pushItems(turtleLocalName, slotNum);
// Get package NBT
logger.debug(
`Turtle:\n${textutils.serialise(blockReader.getBlockData()!, { allow_repetitions: true })}`,
);
const packageDetailInfo = blockReader.getBlockData()?.Items[1];
if (packageDetailInfo === undefined) {
logger.error(`Package detail info not found`);
continue;
}
// Get OrderId and isFinal
const packageOrderId = (packageDetailInfo.tag as CreatePackageTag)
.Fragment.OrderId;
const packageIsFinal =
(packageDetailInfo.tag as CreatePackageTag).Fragment.IsFinal > 0
? true
: false;
// Get recipe
const packageRecipes =
CraftManager.getPackageRecipe(packageDetailInfo);
if (packageRecipes.isSome()) {
if (packageIsFinal) recipesQueue.enqueue(packageRecipes.value);
else recipesWaitingMap.set(packageOrderId, packageRecipes.value);
} else {
if (packageIsFinal && recipesWaitingMap.has(packageOrderId)) {
recipesQueue.enqueue(recipesWaitingMap.get(packageOrderId)!);
recipesWaitingMap.delete(packageOrderId);
} else {
logger.debug(`No recipe, just pass`);
}
}
packageExtractor.pullItems(turtleLocalName, 1);
}
if (
currentState === State.READ_RECIPE &&
nextState === State.CRAFT_OUTPUT
) {
craftManager.initItemsMap();
}
break;
}
case State.CRAFT_OUTPUT: {
// Check recipe
const recipe = recipesQueue.dequeue();
if (recipe === undefined) break;
let restCraftCnt = recipe.Count;
let maxSignleCraftCnt = restCraftCnt;
let craftItemDetail: ItemDetail | undefined = undefined;
do {
// Clear workbench
craftManager.clearTurtle();
logger.info(`Pull items according to a recipe`);
const craftCnt = craftManager
.pullItemsWithRecipe(recipe, maxSignleCraftCnt)
.unwrapOrElse((error) => {
logger.error(error.message);
return 0;
});
if (craftCnt == 0) break;
if (craftCnt < maxSignleCraftCnt) maxSignleCraftCnt = craftCnt;
const craftRet = craftManager.craft(maxSignleCraftCnt);
craftItemDetail ??= craftRet;
logger.info(`Craft ${craftCnt} times`);
restCraftCnt -= craftCnt;
} while (restCraftCnt > 0);
// Finally output
if (restCraftCnt > 0) {
logger.warn(
`Only craft ${recipe.Count - restCraftCnt}x ${craftItemDetail?.name ?? "UnknownItem"}`,
);
} else {
logger.info(
`Finish craft ${recipe.Count}x ${craftItemDetail?.name ?? "UnknownItem"}`,
);
}
// Clear workbench and inventory
const turtleItemSlots = Object.values(
blockReader.getBlockData()!.Items,
).map((val) => val.Slot + 1);
craftManager.clearTurtle(turtleItemSlots);
break;
}
default: {
sleep(1); sleep(1);
break;
}
} }
// Check packages logger.info("AutoCraft init finished...");
hasPackage = redstone.getInput(peripheralsNames.redstone); while (true) {
// State update // Switch state
currentState = nextState; switch (currentState) {
} case State.IDLE: {
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
break;
}
case State.READ_RECIPE: {
nextState = hasPackage ? State.READ_RECIPE : State.CRAFT_OUTPUT;
break;
}
case State.CRAFT_OUTPUT: {
nextState =
recipesQueue.size() > 0
? State.CRAFT_OUTPUT
: hasPackage
? State.READ_RECIPE
: State.IDLE;
break;
}
default: {
logger.error(`Unknown state`);
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
break;
}
}
// State logic
switch (currentState) {
case State.IDLE: {
if (!hasPackage) os.pullEvent("redstone");
hasPackage = redstone.getInput(peripheralsNames.redstone);
break;
}
case State.READ_RECIPE: {
logger.info(`Package detected`);
const packagesInfoRecord = packsInventory.list();
for (const key in packagesInfoRecord) {
const slotNum = parseInt(key);
packsInventory.pushItems(turtleLocalName, slotNum);
// Get package NBT
logger.debug(
`Turtle:\n${textutils.serialise(blockReader.getBlockData()!, { allow_repetitions: true })}`,
);
const packageDetailInfo =
blockReader.getBlockData()?.Items[1];
if (packageDetailInfo === undefined) {
logger.error(`Package detail info not found`);
continue;
}
// Get OrderId and isFinal
const packageOrderId = (
packageDetailInfo.tag as CreatePackageTag
).Fragment.OrderId;
const packageIsFinal =
(packageDetailInfo.tag as CreatePackageTag).Fragment
.IsFinal > 0
? true
: false;
// Get recipe
const packageRecipes =
CraftManager.getPackageRecipe(packageDetailInfo);
if (packageRecipes.isSome()) {
if (packageIsFinal)
recipesQueue.enqueue(packageRecipes.value);
else
recipesWaitingMap.set(
packageOrderId,
packageRecipes.value,
);
} else {
if (
packageIsFinal &&
recipesWaitingMap.has(packageOrderId)
) {
recipesQueue.enqueue(
recipesWaitingMap.get(packageOrderId)!,
);
recipesWaitingMap.delete(packageOrderId);
} else {
logger.debug(`No recipe, just pass`);
}
}
packageExtractor.pullItems(turtleLocalName, 1);
}
if (
currentState === State.READ_RECIPE &&
nextState === State.CRAFT_OUTPUT
) {
craftManager.initItemsMap();
}
break;
}
case State.CRAFT_OUTPUT: {
// Check recipe
const recipe = recipesQueue.dequeue();
if (recipe === undefined) break;
let restCraftCnt = recipe.Count;
let maxSignleCraftCnt = restCraftCnt;
let craftItemDetail: ItemDetail | undefined = undefined;
do {
// Clear workbench
craftManager.clearTurtle();
logger.info(`Pull items according to a recipe`);
const craftCnt = craftManager
.pullItemsWithRecipe(recipe, maxSignleCraftCnt)
.unwrapOrElse((error) => {
logger.error(error.message);
return 0;
});
if (craftCnt == 0) break;
if (craftCnt < maxSignleCraftCnt)
maxSignleCraftCnt = craftCnt;
const craftRet = craftManager.craft(maxSignleCraftCnt);
craftItemDetail ??= craftRet;
logger.info(`Craft ${craftCnt} times`);
restCraftCnt -= craftCnt;
} while (restCraftCnt > 0);
// Finally output
if (restCraftCnt > 0) {
logger.warn(
`Only craft ${recipe.Count - restCraftCnt}x ${craftItemDetail?.name ?? "UnknownItem"}`,
);
} else {
logger.info(
`Finish craft ${recipe.Count}x ${craftItemDetail?.name ?? "UnknownItem"}`,
);
}
// Clear workbench and inventory
const turtleItemSlots = Object.values(
blockReader.getBlockData()!.Items,
).map((val) => val.Slot + 1);
craftManager.clearTurtle(turtleItemSlots);
break;
}
default: {
sleep(1);
break;
}
}
// Check packages
hasPackage = redstone.getInput(peripheralsNames.redstone);
// State update
currentState = nextState;
}
} }
try { try {
main(); main();
} catch (error: unknown) { } catch (error: unknown) {
logger.error(textutils.serialise(error as object)); logger.error(textutils.serialise(error as object));
} finally { } finally {
logger.close(); logger.close();
} }

View File

@@ -7,150 +7,14 @@
*/ */
// Re-export all core types and classes // Re-export all core types and classes
export { export * from "./types";
LogLevel, export * from "./Logger";
LogEvent,
Processor,
Renderer,
Stream,
LoggerOptions,
ILogger,
} from "./types";
export { Logger } from "./Logger";
// Re-export all processors // Re-export all processors
export { export * from "./processors";
addTimestamp,
addFormattedTimestamp,
addFullTimestamp,
filterByLevel,
addSource,
addComputerId,
addComputerLabel,
filterBy,
transformField,
removeFields,
addStaticFields,
} from "./processors";
// Re-export all renderers // Re-export all renderers
export { jsonRenderer, textRenderer } from "./renderers"; export * from "./renderers";
// Re-export all streams // Re-export all streams
export { export * from "./streams";
ConsoleStream,
FileStream,
BufferStream,
NullStream,
SECOND,
MINUTE,
HOUR,
DAY,
WEEK,
} from "./streams";
import { Logger } from "./Logger";
import { LogLevel, LogEvent } from "./types";
import {
addFormattedTimestamp,
addFullTimestamp,
addComputerId,
} from "./processors";
import { textRenderer, jsonRenderer } from "./renderers";
import { ConsoleStream, FileStream, DAY } from "./streams";
/**
* Create a development logger with console output and colored formatting.
*
* This logger is optimized for development and debugging, with:
* - Debug level and above
* - Formatted timestamps
* - Computer ID tracking
* - Human-readable console output with colors
*
* @param options - Optional configuration to override defaults
* @returns A configured Logger instance for development
*/
export function createDevLogger(
options: {
level?: LogLevel;
source?: string;
includeComputerId?: boolean;
} = {},
): Logger {
const processors = [addFormattedTimestamp()];
if (options.includeComputerId !== false) {
processors.push(addComputerId());
}
if (options.source) {
processors.push((event: LogEvent) => {
event.set("source", options.source);
return event;
});
}
return new Logger({
processors,
renderer: textRenderer,
streams: [new ConsoleStream()],
});
}
/**
* Create a production logger with file output and JSON formatting.
*
* This logger is optimized for production environments, with:
* - Info level and above
* - Full timestamps
* - Computer ID and label tracking
* - JSON output for machine processing
* - Daily file rotation
*
* @param filename - Base filename for log files
* @param options - Optional configuration to override defaults
* @returns A configured Logger instance for production
*/
export function createProdLogger(
filename: string,
options: {
level?: LogLevel;
source?: string;
rotationInterval?: number;
includeConsole?: boolean;
} = {},
): Logger {
const processors = [
addFullTimestamp(),
addComputerId(),
(event: LogEvent) => {
const label = os.getComputerLabel();
if (label) {
event.set("computer_label", label);
}
return event;
},
];
if (options.source) {
processors.push((event: LogEvent) => {
event.set("source", options.source);
return event;
});
}
const streams: Array<ConsoleStream | FileStream> = [
new FileStream(filename, options.rotationInterval ?? DAY),
];
if (options.includeConsole) {
streams.push(new ConsoleStream());
}
return new Logger({
processors,
renderer: jsonRenderer,
streams,
});
}

View File

@@ -8,213 +8,196 @@
import { LogEvent, Processor, LogLevel } from "./types"; import { LogEvent, Processor, LogLevel } from "./types";
/** export namespace processor {
* Adds a timestamp to the log event. /**
* * Configuration options for the timestamp processor.
* This processor adds the current time as a structured timestamp object */
* using CC:Tweaked's os.date() function. The timestamp includes year, interface TimestampConfig {
* month, day, hour, minute, and second components. /**
* * The format string takes the same formats as C's strftime function.
* Performance note: os.date() is relatively expensive, so this should */
* typically be placed early in the processor chain and used only once. format?: string;
* }
* @param event - The log event to process
* @returns The event with timestamp added /**
*/ * Adds a timestamp to each log event.
export function addTimestamp(): Processor { *
return (event) => { * This processor adds a "time" field to each log event with the current
const timestamp = os.date("!*t") as LuaDate; * timestamp. The timestamp format can be customized using the `format`
event.set("timestamp", timestamp); * option.
return event; *
}; * @param config - Configuration options for the timestamp processor.
} * @returns A processor function that adds a timestamp to each log event.
*/
/** export function addTimestamp(config: TimestampConfig = {}): Processor {
* Adds a human-readable timestamp string to the log event. return (event) => {
* let time: string;
* This processor adds a formatted timestamp string that's easier to read if (config.format === undefined) {
* in log output. The format is "HH:MM:SS" in UTC time. time = os.date("%F %T") as string;
* } else {
* @param event - The log event to process time = os.date(config.format) as string;
* @returns The event with formatted timestamp added }
*/
export function addFormattedTimestamp(): Processor { event.set("timestamp", time);
return (event) => { return event;
const timestamp = os.date("!*t") as LuaDate; };
const timeStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`; }
event.set("time", timeStr);
return event; /**
}; * Filters log events by minimum level.
} *
* This processor drops log events that are below the specified minimum level.
/** * Note: The Logger class already does early filtering for performance, but
* Adds a full ISO-like timestamp string to the log event. * this processor can be useful for dynamic filtering or when you need
* * different levels for different streams.
* This processor adds a complete timestamp in YYYY-MM-DD HH:MM:SS format *
* which is useful for file logging and structured output. * @param minLevel - The minimum log level to allow through
* * @returns A processor function that filters by level
* @param event - The log event to process */
* @returns The event with full timestamp added export function filterByLevel(minLevel: LogLevel): Processor {
*/ return (event) => {
export function addFullTimestamp(): Processor { const eventLevel = event.get("level") as LogLevel | undefined;
return (event) => { if (eventLevel === undefined) {
const timestamp = os.date("!*t") as LuaDate; return event; // Pass through if no level is set
const fullTimeStr = `${timestamp.year}-${string.format("%02d", timestamp.month)}-${string.format("%02d", timestamp.day)} ${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`; }
event.set("datetime", fullTimeStr);
return event; if (eventLevel !== undefined && eventLevel < minLevel) {
}; return undefined; // Drop the log event
} }
/** return event;
* Filters log events by minimum level. };
* }
* This processor drops log events that are below the specified minimum level.
* Note: The Logger class already does early filtering for performance, but /**
* this processor can be useful for dynamic filtering or when you need * Adds a logger name/source to the log event.
* different levels for different streams. *
* * This processor is useful when you have multiple loggers in your application
* @param minLevel - The minimum log level to allow through * and want to identify which component generated each log entry.
* @returns A processor function that filters by level *
*/ * @param name - The name/source to add to log events
export function filterByLevel(minLevel: LogLevel): Processor { * @returns A processor function that adds the source name
return (event) => { */
const eventLevel = event.get("level") as LogLevel | undefined; export function addSource(name: string): Processor {
if (eventLevel === undefined) { return (event) => {
return event; // Pass through if no level is set event.set("source", name);
} return event;
};
if (eventLevel !== undefined && eventLevel < minLevel) { }
return undefined; // Drop the log event
} /**
* Adds the current computer ID to the log event.
return event; *
}; * In CC:Tweaked environments, this can help identify which computer
} * generated the log when logs are aggregated from multiple sources.
*
/** * @param event - The log event to process
* Adds a logger name/source to the log event. * @returns The event with computer ID added
* */
* This processor is useful when you have multiple loggers in your application export function addComputerId(): Processor {
* and want to identify which component generated each log entry. return (event) => {
* event.set("computer_id", os.getComputerID());
* @param name - The name/source to add to log events return event;
* @returns A processor function that adds the source name };
*/ }
export function addSource(name: string): Processor {
return (event) => { /**
event.set("source", name); * Adds the current computer label to the log event.
return event; *
}; * If the computer has a label set, this adds it to the log event.
} * This can be more human-readable than the computer ID.
*
/** * @param event - The log event to process
* Adds the current computer ID to the log event. * @returns The event with computer label added (if available)
* */
* In CC:Tweaked environments, this can help identify which computer export function addComputerLabel(): Processor {
* generated the log when logs are aggregated from multiple sources. return (event) => {
* const label = os.getComputerLabel();
* @param event - The log event to process if (label !== undefined && label !== null) {
* @returns The event with computer ID added event.set("computer_label", label);
*/ }
export function addComputerId(): Processor { return event;
return (event) => { };
event.set("computer_id", os.getComputerID()); }
return event;
}; /**
} * Filters out events that match a specific condition.
*
/** * This is a generic processor that allows you to filter events based on
* Adds the current computer label to the log event. * any custom condition. The predicate function should return true to keep
* * the event and false to drop it.
* If the computer has a label set, this adds it to the log event. *
* This can be more human-readable than the computer ID. * @param predicate - Function that returns true to keep the event
* * @returns A processor function that filters based on the predicate
* @param event - The log event to process */
* @returns The event with computer label added (if available) export function filterBy(
*/ predicate: (event: LogEvent) => boolean,
export function addComputerLabel(): Processor { ): Processor {
return (event) => { return (event) => {
const label = os.getComputerLabel(); return predicate(event) ? event : undefined;
if (label !== undefined && label !== null) { };
event.set("computer_label", label); }
}
return event; /**
}; * Transforms a specific field in the log event.
} *
* This processor allows you to modify the value of a specific field
/** * using a transformation function.
* Filters out events that match a specific condition. *
* * @param fieldName - The name of the field to transform
* This is a generic processor that allows you to filter events based on * @param transformer - Function to transform the field value
* any custom condition. The predicate function should return true to keep * @returns A processor function that transforms the specified field
* the event and false to drop it. */
* export function transformField(
* @param predicate - Function that returns true to keep the event fieldName: string,
* @returns A processor function that filters based on the predicate transformer: (value: unknown) => unknown,
*/ ): Processor {
export function filterBy(predicate: (event: LogEvent) => boolean): Processor { return (event) => {
return (event) => { if (event.has(fieldName)) {
return predicate(event) ? event : undefined; const currentValue = event.get(fieldName);
}; const newValue = transformer(currentValue);
} event.set(fieldName, newValue);
}
/** return event;
* Transforms a specific field in the log event. };
* }
* This processor allows you to modify the value of a specific field
* using a transformation function. /**
* * Removes specified fields from the log event.
* @param fieldName - The name of the field to transform *
* @param transformer - Function to transform the field value * This processor can be used to strip sensitive or unnecessary information
* @returns A processor function that transforms the specified field * from log events before they are rendered and output.
*/ *
export function transformField( * @param fieldNames - Array of field names to remove
fieldName: string, * @returns A processor function that removes the specified fields
transformer: (value: unknown) => unknown, */
): Processor { export function removeFields(fieldNames: string[]): Processor {
return (event) => { return (event) => {
if (event.has(fieldName)) { for (const fieldName of fieldNames) {
const currentValue = event.get(fieldName); event.delete(fieldName);
const newValue = transformer(currentValue); }
event.set(fieldName, newValue); return event;
} };
return event; }
};
} /**
* Adds static fields to every log event.
/** *
* Removes specified fields from the log event. * This processor adds the same set of fields to every log event that
* * passes through it. Useful for adding application name, version,
* This processor can be used to strip sensitive or unnecessary information * environment, etc.
* from log events before they are rendered and output. *
* * @param fields - Object containing the static fields to add
* @param fieldNames - Array of field names to remove * @returns A processor function that adds the static fields
* @returns A processor function that removes the specified fields */
*/ export function addStaticFields(
export function removeFields(fieldNames: string[]): Processor { fields: Record<string, unknown>,
return (event) => { ): Processor {
for (const fieldName of fieldNames) { return (event) => {
event.delete(fieldName); for (const [key, value] of Object.entries(fields)) {
} event.set(key, value);
return event; }
}; return event;
} };
}
/**
* Adds static fields to every log event.
*
* This processor adds the same set of fields to every log event that
* passes through it. Useful for adding application name, version,
* environment, etc.
*
* @param fields - Object containing the static fields to add
* @returns A processor function that adds the static fields
*/
export function addStaticFields(fields: Record<string, unknown>): Processor {
return (event) => {
for (const [key, value] of Object.entries(fields)) {
event.set(key, value);
}
return event;
};
} }

View File

@@ -6,7 +6,7 @@
* different output formats (JSON, console-friendly, etc.). * different output formats (JSON, console-friendly, etc.).
*/ */
import { Renderer } from "./types"; import { LogLevel, Renderer } from "./types";
/** /**
* Renders log events as JSON strings. * Renders log events as JSON strings.
@@ -43,44 +43,30 @@ export const jsonRenderer: Renderer = (event) => {
* timestamp, level, message, and additional context fields formatted * timestamp, level, message, and additional context fields formatted
* in a readable way. * in a readable way.
* *
* Format: [HH:MM:SS] [LEVEL] message { key=value, key2=value2 } * Format: [YYYY-MM-DD HH:MM:SS] [LEVEL] message key=value, key2=value2
* *
* @param event - The log event to render * @param event - The log event to render
* @returns Human-readable string representation * @returns Human-readable string representation
*/ */
export const textRenderer: Renderer = (event) => { export const textRenderer: Renderer = (event) => {
// Extract core components // Extract core components
const timestamp = event.get("timestamp") as LuaDate | undefined; const timeStr = event.get("timestamp") as string | undefined;
const timeStr = event.get("time") as string | undefined; const level: string | undefined = LogLevel[event.get("level") as LogLevel];
const level = (event.get("level") as string)?.toUpperCase() ?? "UNKNOWN";
const message = (event.get("message") as string) ?? ""; const message = (event.get("message") as string) ?? "";
// Format timestamp
let timestampStr = "";
if (timeStr) {
timestampStr = timeStr;
} else if (timestamp) {
timestampStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
}
// Start building the output // Start building the output
let output = `[${timestampStr}] [${level}] ${message}`; let output = `[${timeStr}] [${level}] ${message} \t`;
// Add context fields (excluding the core fields we already used) // Add context fields (excluding the core fields we already used)
const contextFields: string[] = []; const contextFields: string[] = [];
for (const [key, value] of event.entries()) { for (const [key, value] of event.entries()) {
if ( if (key !== "timestamp" && key !== "level" && key !== "message") {
key !== "timestamp" &&
key !== "time" &&
key !== "level" &&
key !== "message"
) {
contextFields.push(`${key}=${tostring(value)}`); contextFields.push(`${key}=${tostring(value)}`);
} }
} }
if (contextFields.length > 0) { if (contextFields.length > 0) {
output += ` { ${contextFields.join(", ")} }`; output += contextFields.join(", ");
} }
return output; return output;

View File

@@ -6,7 +6,7 @@
* implements the Stream interface and handles its own output logic. * implements the Stream interface and handles its own output logic.
*/ */
import { Stream, LogEvent } from "./types"; import { LogLevel, Stream, LogEvent } from "./types";
/** /**
* Console stream that outputs to the CC:Tweaked terminal. * Console stream that outputs to the CC:Tweaked terminal.
@@ -16,14 +16,14 @@ import { Stream, LogEvent } from "./types";
* color after writing each message. * color after writing each message.
*/ */
export class ConsoleStream implements Stream { export class ConsoleStream implements Stream {
private levelColors: Map<string, number> = new Map([ private levelColors: { [key: string]: number } = {
["trace", colors.lightGray], Trace: colors.lightGray,
["debug", colors.gray], Debug: colors.gray,
["info", colors.white], Info: colors.green,
["warn", colors.orange], Warn: colors.orange,
["error", colors.red], Error: colors.red,
["fatal", colors.red], Fatal: colors.red,
]); };
/** /**
* Write a formatted log message to the terminal. * Write a formatted log message to the terminal.
@@ -32,8 +32,9 @@ export class ConsoleStream implements Stream {
* @param event - The original log event for context (used for level-based coloring) * @param event - The original log event for context (used for level-based coloring)
*/ */
public write(message: string, event: LogEvent): void { public write(message: string, event: LogEvent): void {
const level = event.get("level") as string | undefined; const level: string | undefined =
const color = level ? this.levelColors.get(level) : undefined; LogLevel[event.get("level") as LogLevel];
const color = level !== undefined ? this.levelColors[level] : undefined;
if (color !== undefined) { if (color !== undefined) {
const originalColor = term.getTextColor(); const originalColor = term.getTextColor();

View File

@@ -7,56 +7,16 @@
import { import {
Logger, Logger,
createDevLogger, processor,
createProdLogger,
// Processors
addTimestamp,
addFormattedTimestamp,
addFullTimestamp,
addSource,
addComputerId,
addStaticFields,
transformField,
// Renderers
textRenderer,
jsonRenderer,
// Streams
ConsoleStream, ConsoleStream,
FileStream, FileStream,
BufferStream, BufferStream,
DAY, ConditionalStream,
HOUR, HOUR,
jsonRenderer,
LogLevel, LogLevel,
textRenderer,
} from "../lib/ccStructLog"; } from "../lib/ccStructLog";
import { ConditionalStream } from "@/lib/ccStructLog/streams";
// =============================================================================
// Basic Usage Examples
// =============================================================================
print("=== Basic Usage Examples ===");
// 1. Quick start with pre-configured loggers
const devLog = createDevLogger();
devLog.info("Application started", { version: "1.0.0", port: 8080 });
devLog.debug("Debug information", { userId: 123, action: "login" });
devLog.error("Something went wrong", {
error: "Connection failed",
retries: 3,
});
// 2. Production logging to file
const prodLog = createProdLogger("app.log", {
source: "MyApplication",
rotationInterval: DAY,
includeConsole: true,
});
prodLog.info("User action", { userId: 456, action: "purchase", amount: 29.99 });
prodLog.warn("Low disk space", { available: 1024, threshold: 2048 });
// ============================================================================= // =============================================================================
// Custom Logger Configurations // Custom Logger Configurations
@@ -67,10 +27,10 @@ print("\n=== Custom Logger Configurations ===");
// 4. Custom logger with specific processors and renderer // 4. Custom logger with specific processors and renderer
const customLogger = new Logger({ const customLogger = new Logger({
processors: [ processors: [
addFullTimestamp(), processor.addTimestamp(),
addComputerId(), processor.addComputerId(),
addSource("CustomApp"), processor.addSource("CustomApp"),
addStaticFields({ processor.addStaticFields({
environment: "development", environment: "development",
version: "2.1.0", version: "2.1.0",
}), }),
@@ -109,10 +69,10 @@ const sanitizePasswords = (event: Map<string, unknown>) => {
const secureLogger = new Logger({ const secureLogger = new Logger({
processors: [ processors: [
addTimestamp(), processor.addTimestamp(),
addRequestId, addRequestId,
sanitizePasswords, sanitizePasswords,
transformField("message", (msg) => `[SECURE] ${msg}`), processor.transformField("message", (msg) => `[SECURE] ${msg}`),
], ],
renderer: jsonRenderer, renderer: jsonRenderer,
streams: [new ConsoleStream()], streams: [new ConsoleStream()],
@@ -133,7 +93,7 @@ print("\n=== Stream Examples ===");
// 11. Buffer stream for batch processing // 11. Buffer stream for batch processing
const bufferStream = new BufferStream(100); // Keep last 100 messages const bufferStream = new BufferStream(100); // Keep last 100 messages
const bufferLogger = new Logger({ const bufferLogger = new Logger({
processors: [addFormattedTimestamp()], processors: [processor.addTimestamp()],
renderer: textRenderer, renderer: textRenderer,
streams: [ streams: [
new ConditionalStream(new ConsoleStream(), (msg, event) => { new ConditionalStream(new ConsoleStream(), (msg, event) => {
@@ -162,7 +122,7 @@ for (const msg of bufferedMessages) {
// 12. Multi-stream with different formats // 12. Multi-stream with different formats
const multiFormatLogger = new Logger({ const multiFormatLogger = new Logger({
processors: [addFullTimestamp(), addComputerId()], processors: [processor.addTimestamp(), processor.addComputerId()],
renderer: (event) => "default", // This won't be used renderer: (event) => "default", // This won't be used
streams: [ streams: [
// Console with human-readable format // Console with human-readable format
@@ -196,7 +156,7 @@ print("\n=== Error Handling Examples ===");
// 13. Robust error handling // 13. Robust error handling
const robustLogger = new Logger({ const robustLogger = new Logger({
processors: [ processors: [
addTimestamp(), processor.addTimestamp(),
// Processor that might fail // Processor that might fail
(event) => { (event) => {
try { try {
@@ -231,7 +191,7 @@ print("\n=== Cleanup Examples ===");
// 14. Proper cleanup // 14. Proper cleanup
const fileLogger = new Logger({ const fileLogger = new Logger({
processors: [addTimestamp()], processors: [processor.addTimestamp()],
renderer: jsonRenderer, renderer: jsonRenderer,
streams: [new FileStream("temp.log")], streams: [new FileStream("temp.log")],
}); });
@@ -249,37 +209,3 @@ print("- all.log (complete log)");
print("- debug.log (detailed debug info)"); print("- debug.log (detailed debug info)");
print("- structured.log (JSON format)"); print("- structured.log (JSON format)");
print("- temp.log (temporary file, now closed)"); print("- temp.log (temporary file, now closed)");
// =============================================================================
// Performance Comparison (commented out to avoid noise)
// =============================================================================
/*
print("\n=== Performance Comparison ===");
const iterations = 1000;
// Test simple console logging
const startTime1 = os.clock();
const simpleLogger = createMinimalLogger();
for (let i = 0; i < iterations; i++) {
simpleLogger.info(`Simple message ${i}`);
}
const endTime1 = os.clock();
print(`Simple Console Logger: ${endTime1 - startTime1} seconds`);
// Test complex processor chain
const startTime2 = os.clock();
const complexLogger = createDetailedLogger("perf_test.log", {
source: "PerfTest"
});
for (let i = 0; i < iterations; i++) {
complexLogger.info(`Complex message ${i}`, {
iteration: i,
data: { nested: { value: i * 2 } }
});
}
complexLogger.close();
const endTime2 = os.clock();
print(`Complex Processor Chain: ${endTime2 - startTime2} seconds`);
*/