mirror of
https://github.com/SikongJueluo/kubejs-utils.git
synced 2025-11-18 19:37:50 +08:00
feature(custom_command): add areacontrol command
feature(areacontrol): - cubic detection - auto-change player gamemode
This commit is contained in:
168
docs/areacontrol.md
Normal file
168
docs/areacontrol.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# AreaControl 脚本 MDocs
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
**AreaControl** 是一个为 Minecraft 服务器设计的强大区域管理工具,通过 KubeJS 实现。它允许管理员在游戏世界中定义一个特殊区域,并对进入该区域的玩家施加特定的规则,例如自动切换游戏模式、限制物品使用等。
|
||||||
|
|
||||||
|
该脚本的核心设计理念是 **高性能** 与 **易用性**。它采用事件驱动和多种优化技术,确保在不牺牲服务器性能的前提下,提供稳定可靠的功能。本文档将为您提供从安装、使用到二次开发的全方位指南。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 功能特性
|
||||||
|
|
||||||
|
- **自动模式切换**:玩家进入指定区域时,自动切换为**冒险**或**旁观**模式;离开时恢复为**生存**模式。
|
||||||
|
- **动态白名单**:所有功能仅对白名单内的玩家生效,管理员可通过命令随时增删玩家。
|
||||||
|
- **物品冷却系统**:可以为区域内的玩家设置统一的物品使用冷却时间。
|
||||||
|
- **实时启/禁用**:管理员可通过一条简单命令,在不重启服务器的情况下,全局启用或禁用所有功能。
|
||||||
|
- **二维平面检测**:区域检测仅基于水平坐标(X 和 Z 轴),忽略玩家的高度(Y 轴),适用于各种地形。
|
||||||
|
- **高性能设计**:基于事件驱动,无不必要的循环(tick-polling),确保对服务器的性能影响降至最低。
|
||||||
|
- **配置持久化**:所有配置(如区域中心、半径、白名单)都会自动保存,服务器重启后无需重新设置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 用户指南
|
||||||
|
|
||||||
|
本节面向服务器管理员和普通用户,指导您如何安装和使用 AreaControl。
|
||||||
|
|
||||||
|
### 3.1. 安装
|
||||||
|
|
||||||
|
1. 确保您的 Minecraft 服务器已经正确安装了 KubeJS Mod。
|
||||||
|
2. 将编译后的 `areacontrol.js` 脚本文件放置在服务器的 `kubejs/server_scripts/` 目录下。
|
||||||
|
3. 重新启动服务器或在游戏内执行 `/kubejs reload server_scripts`。
|
||||||
|
|
||||||
|
脚本加载后将自动初始化,默认在世界中心 `(0, 0)` 创建一个半径为 `50` 格的区域。
|
||||||
|
|
||||||
|
### 3.2. 管理命令
|
||||||
|
|
||||||
|
您可以在游戏内通过 `areacontrol` 系列命令来管理脚本。所有命令都需要管理员权限。
|
||||||
|
|
||||||
|
* `/areacontrol status`:查看脚本当前配置。
|
||||||
|
* `/areacontrol toggle`:全局启用或禁用功能。
|
||||||
|
* `/areacontrol setcenter`:将当前位置设为区域中心。
|
||||||
|
* `/areacontrol setradius <半径>`:设置区域半径。
|
||||||
|
* `/areacontrol whitelist <add|remove|list> [玩家名]`:管理白名单。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 开发者参考:技术与定制
|
||||||
|
|
||||||
|
本节面向希望理解其工作原理或进行二次开发的开发者。
|
||||||
|
|
||||||
|
### 4.1. 开发环境与构建
|
||||||
|
|
||||||
|
本项目使用 **TypeScript** 编写,以获得更强的类型安全和代码可维护性。
|
||||||
|
|
||||||
|
- **源码目录**:所有服务器端脚本的 TypeScript 源文件位于 `src/server_scripts/` 目录下。
|
||||||
|
- **编译**:在发布或测试前,需要将 TypeScript (`.ts`) 文件编译为 KubeJS 可识别的 JavaScript (`.js`) 文件。
|
||||||
|
- **单次编译**:执行 `npm run tsc` 或 `npx tsc --project tsconfig.server.json`。
|
||||||
|
- **监视模式**:在开发过程中,建议使用监视模式,它会在文件发生变化时自动重新编译。执行 `npm run watch::server` 即可。
|
||||||
|
- **类型定义**:项目依赖于 KubeJS Probe 生成的类型定义,位于 `types/probe-types/` 目录,这为开发提供了完整的代码提示和类型检查。
|
||||||
|
|
||||||
|
### 4.2. 设计理念
|
||||||
|
|
||||||
|
#### 事件驱动架构
|
||||||
|
脚本不使用高开销的 `tick` 轮询,而是监听特定玩家事件来触发逻辑。
|
||||||
|
- `PlayerEvents.loggedIn`: 玩家登录时加入检查。
|
||||||
|
- `PlayerEvents.loggedOut`: 玩家登出时清理其缓存数据。
|
||||||
|
- `PlayerEvents.tick`: **降频使用**,每秒检查一次玩家位置。
|
||||||
|
- `ItemEvents.use`: 在玩家使用物品时触发冷却逻辑。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 示例:利用类型定义,精确捕获事件和玩家对象
|
||||||
|
ItemEvents.use((event: Internal.ItemUseEvent) => {
|
||||||
|
const { player } = event;
|
||||||
|
if (player && shouldApplyCooldown(player)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 分层与状态缓存
|
||||||
|
为避免不必要的操作,脚本采用分层检查和状态缓存。
|
||||||
|
|
||||||
|
1. **一级过滤**:检查脚本是否启用、玩家是否在白名单内。
|
||||||
|
2. **二级检查(降频)**:每秒检查一次玩家是否在区域内。
|
||||||
|
3. **三级处理(状态驱动)**:仅当玩家**跨越区域边界**时,才执行核心操作并更新缓存。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// playerStates 缓存玩家的当前状态,并提供类型安全
|
||||||
|
const playerStates = new Map<string, { inArea: boolean }>();
|
||||||
|
|
||||||
|
function optimizedPlayerCheck(player: Internal.Player): void {
|
||||||
|
const currentState = playerStates.get(player.uuid) ?? { inArea: false };
|
||||||
|
const isInArea = isPlayerInArea(player);
|
||||||
|
|
||||||
|
if (currentState.inArea !== isInArea) {
|
||||||
|
handleGameModeChange(player, isInArea);
|
||||||
|
playerStates.set(player.uuid, { inArea: isInArea });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3. 核心算法
|
||||||
|
|
||||||
|
#### 快速边界检测
|
||||||
|
脚本通过**预计算**区域的边界框(Bounding Box)来实现高效的位置判断。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 初始化时预计算边界
|
||||||
|
function updateBounds(center: Internal.Vec3i, radius: number): void {
|
||||||
|
bounds.minX = center.x - radius;
|
||||||
|
bounds.maxX = center.x + radius;
|
||||||
|
bounds.minZ = center.z - radius;
|
||||||
|
bounds.maxZ = center.z + radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 运行时进行高效比较
|
||||||
|
function isPlayerInArea(player: Internal.Player): boolean {
|
||||||
|
const pos = player.blockPosition();
|
||||||
|
return pos.x >= bounds.minX && pos.x <= bounds.maxX &&
|
||||||
|
pos.z >= bounds.minZ && pos.z <= bounds.maxZ;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 内存自动清理
|
||||||
|
通过监听 `PlayerEvents.loggedOut` 事件,自动从 `playerStates` 缓存中删除下线玩家的数据,防止内存泄漏。
|
||||||
|
|
||||||
|
### 4.4. 数据结构
|
||||||
|
|
||||||
|
我们使用 TypeScript 的 `interface` 来定义核心数据结构,确保类型安全。
|
||||||
|
|
||||||
|
#### 配置 (AreaControlConfig)
|
||||||
|
```typescript
|
||||||
|
interface AreaControlConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
center: { x: number; y: number; z: number };
|
||||||
|
radius: number;
|
||||||
|
whitelist: string[];
|
||||||
|
mode: 'adventure' | 'spectator';
|
||||||
|
cooldownTime: number; // In ticks
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 缓存 (PlayerState)
|
||||||
|
```typescript
|
||||||
|
interface PlayerState {
|
||||||
|
inArea: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终的缓存结构
|
||||||
|
const playerStates: Map<string, PlayerState>; // Key: Player UUID
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5. 定制化
|
||||||
|
|
||||||
|
#### 调整检查频率
|
||||||
|
默认检查频率是每秒一次(`20 ticks`)。您可以根据服务器需求调整此值。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 PlayerEvents.tick 监听器中调整
|
||||||
|
PlayerEvents.tick((event: Internal.PlayerTickEvent) => {
|
||||||
|
const player = event.player;
|
||||||
|
// 将 20 修改为您希望的检查间隔(ticks)
|
||||||
|
if (player.age % 20 === 0) {
|
||||||
|
optimizedPlayerCheck(player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
- **建议值**:`10`(半秒一次,响应快),`40`(两秒一次,开销低)。不建议低于 `10`。
|
||||||
705
src/server_scripts/areacontrol.js
Normal file
705
src/server_scripts/areacontrol.js
Normal file
@@ -0,0 +1,705 @@
|
|||||||
|
// AreaControl - Advanced Area Management System for KubeJS
|
||||||
|
// Event-driven architecture with high performance optimization
|
||||||
|
|
||||||
|
// ==================== TYPE DEFINITIONS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} AreaControlConfig
|
||||||
|
* @property {boolean} enabled
|
||||||
|
* @property {{x: number, y: number, z: number}} center
|
||||||
|
* @property {number} radius
|
||||||
|
* @property {string[]} whitelist
|
||||||
|
* @property {"adventure" | "spectator"} mode
|
||||||
|
* @property {number} cooldownSecs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} AreaBounds
|
||||||
|
* @property {number} minX
|
||||||
|
* @property {number} maxX
|
||||||
|
* @property {number} minZ
|
||||||
|
* @property {number} maxZ
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {typeof Internal.HashMap} HashMap
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== GLOBAL CONSTANTS ====================
|
||||||
|
|
||||||
|
const SECOND_TICKS = 20;
|
||||||
|
const CONFIG_FILE = "areacontrol_config.json";
|
||||||
|
const CHECK_FREQUENCY = 20; // ticks (1 second)
|
||||||
|
|
||||||
|
// ==================== STATE MANAGEMENT ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration
|
||||||
|
* @type {AreaControlConfig}
|
||||||
|
*/
|
||||||
|
let config = {
|
||||||
|
enabled: true,
|
||||||
|
center: { x: 0, y: 0, z: 0 },
|
||||||
|
radius: 5,
|
||||||
|
whitelist: [],
|
||||||
|
mode: "adventure",
|
||||||
|
cooldownSecs: 10 * SECOND_TICKS, // 60 seconds
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-calculated bounds for O(1) area checking
|
||||||
|
* @type {AreaBounds}
|
||||||
|
*/
|
||||||
|
const bounds = {
|
||||||
|
minX: -50,
|
||||||
|
maxX: 50,
|
||||||
|
minZ: -50,
|
||||||
|
maxZ: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player state cache - prevents unnecessary operations
|
||||||
|
* @type {{[key: string]: boolean | undefined}}
|
||||||
|
*/
|
||||||
|
const playerStates = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item cooldown tracking
|
||||||
|
* @type {{[key: string]: number | undefined}}
|
||||||
|
*/
|
||||||
|
const playerCooldowns = {};
|
||||||
|
|
||||||
|
// ==================== UTILITY FUNCTIONS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update area bounds based on center and radius
|
||||||
|
* Pre-calculates boundaries for efficient checking
|
||||||
|
* @param {{x: number, y: number, z: number}} center
|
||||||
|
* @param {number} radius
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function updateBounds(center, radius) {
|
||||||
|
bounds.minX = center.x - radius;
|
||||||
|
bounds.maxX = center.x + radius;
|
||||||
|
bounds.minZ = center.z - radius;
|
||||||
|
bounds.maxZ = center.z + radius;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[AreaControl] Updated bounds: X(${String(bounds.minX)} to ${String(bounds.maxX)}), Z(${String(bounds.minZ)} to ${String(bounds.maxZ)})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fast 2D area boundary check (ignores Y coordinate)
|
||||||
|
* Uses pre-calculated bounds for O(1) performance
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} z
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isPositionInArea(x, z) {
|
||||||
|
return (
|
||||||
|
x >= bounds.minX &&
|
||||||
|
x <= bounds.maxX &&
|
||||||
|
z >= bounds.minZ &&
|
||||||
|
z <= bounds.maxZ
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if player is whitelisted for area control
|
||||||
|
* @param {string} playerName
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isPlayerWhitelisted(playerName) {
|
||||||
|
return config.whitelist.indexOf(playerName) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle player entering the protected area
|
||||||
|
* @param {Internal.Player} player
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function handlePlayerEnterArea(player) {
|
||||||
|
// Apply configured game mode
|
||||||
|
if (config.mode === "adventure") {
|
||||||
|
Utils.getServer().getPlayer(player.stringUuid).setGameMode("adventure");
|
||||||
|
} else {
|
||||||
|
Utils.getServer().getPlayer(player.stringUuid).setGameMode("spectator");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
player.tell(
|
||||||
|
/** @type {any} */ (
|
||||||
|
Component.string(
|
||||||
|
"§6[AreaControl] §eEntered protected area. Game mode changed.",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle player leaving the protected area
|
||||||
|
* @param {Internal.Player} player
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function handlePlayerLeaveArea(player) {
|
||||||
|
// Restore survival mode
|
||||||
|
Utils.getServer().getPlayer(player.stringUuid).setGameMode("survival");
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
player.tell(
|
||||||
|
/** @type {any} */ (
|
||||||
|
Component.string(
|
||||||
|
"§6[AreaControl] §eLeft protected area. Game mode restored.",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized player area check with state caching
|
||||||
|
* Only triggers changes when crossing area boundaries
|
||||||
|
* @param {Internal.Player} player
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function checkPlayerAreaStatus(player) {
|
||||||
|
if (!config.enabled || !isPlayerWhitelisted(player.username)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = player.blockPosition();
|
||||||
|
const isCurrentlyInArea = isPositionInArea(pos.x, pos.z);
|
||||||
|
const playerId = player.stringUuid;
|
||||||
|
const cachedState = playerStates[playerId];
|
||||||
|
|
||||||
|
// Only process if state changed or first check
|
||||||
|
if (cachedState !== isCurrentlyInArea) {
|
||||||
|
if (isCurrentlyInArea) {
|
||||||
|
handlePlayerEnterArea(player);
|
||||||
|
} else if (cachedState === true) {
|
||||||
|
// Only trigger leave if we were previously in area
|
||||||
|
handlePlayerLeaveArea(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached state
|
||||||
|
playerStates[playerId] = isCurrentlyInArea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if item cooldown should be applied
|
||||||
|
* @param {Internal.Player} player
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function shouldApplyItemCooldown(player) {
|
||||||
|
if (!config.enabled || !isPlayerWhitelisted(player.username)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = playerStates[player.stringUuid];
|
||||||
|
return playerState === undefined || playerState === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save configuration to persistent storage
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function saveConfiguration() {
|
||||||
|
const server = Utils.server;
|
||||||
|
try {
|
||||||
|
// Use KubeJS persistent data instead of JsonIO
|
||||||
|
if (server.persistentData.contains(CONFIG_FILE)) {
|
||||||
|
server.persistentData.put(CONFIG_FILE, NBT.toTag(config));
|
||||||
|
console.log("[AreaControl] Configuration saved successfully");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[AreaControl] Failed to save configuration:${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from persistent storage
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function loadConfiguration() {
|
||||||
|
const server = Utils.server;
|
||||||
|
try {
|
||||||
|
if (server.persistentData.contains(CONFIG_FILE)) {
|
||||||
|
const savedData = server.persistentData.get(CONFIG_FILE);
|
||||||
|
if (typeof savedData === "string") {
|
||||||
|
const loadedConfig = JSON.parse(savedData);
|
||||||
|
config = Object.assign(config, loadedConfig);
|
||||||
|
updateBounds(config.center, config.radius);
|
||||||
|
console.log("[AreaControl] Configuration loaded from file");
|
||||||
|
} else {
|
||||||
|
updateBounds(config.center, config.radius);
|
||||||
|
saveConfiguration(); // Create initial config
|
||||||
|
console.log("[AreaControl] Created default configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`[AreaControl] Failed to load configuration, using defaults: ${error}`,
|
||||||
|
);
|
||||||
|
updateBounds(config.center, config.radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all event handlers
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function registerEventHandlers() {
|
||||||
|
/**
|
||||||
|
* @param {Internal.PlayerEvent.LoggedIn} event
|
||||||
|
*/
|
||||||
|
PlayerEvents.loggedIn((event) => {
|
||||||
|
const { player } = event;
|
||||||
|
|
||||||
|
const pos = player.blockPosition();
|
||||||
|
const isInArea = isPositionInArea(pos.x, pos.z);
|
||||||
|
|
||||||
|
playerStates[player.stringUuid] = isInArea;
|
||||||
|
|
||||||
|
// Apply immediate game mode if in area
|
||||||
|
if (isInArea && config.enabled) {
|
||||||
|
handlePlayerEnterArea(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[AreaControl] Player ${player.username} logged in, in area: ${String(isInArea)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Internal.PlayerEvent.LoggedOut} event
|
||||||
|
*/
|
||||||
|
PlayerEvents.loggedOut((event) => {
|
||||||
|
const { player } = event;
|
||||||
|
const playerId = player.stringUuid;
|
||||||
|
|
||||||
|
delete playerStates[playerId];
|
||||||
|
delete playerCooldowns[playerId];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[AreaControl] Cleaned up data for player ${player.username}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Internal.PlayerEvent.Tick} event
|
||||||
|
*/
|
||||||
|
PlayerEvents.tick((event) => {
|
||||||
|
const { player } = event;
|
||||||
|
|
||||||
|
// Check every CHECK_FREQUENCY ticks for performance
|
||||||
|
if (player.age % CHECK_FREQUENCY === 0) {
|
||||||
|
checkPlayerAreaStatus(player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Internal.LivingEntityUseItemEvent$Finish} event
|
||||||
|
*/
|
||||||
|
// ForgeEvents.onEvent(
|
||||||
|
// "net.minecraftforge.event.entity.living.LivingEntityUseItemEvent$Finish",
|
||||||
|
// (event) => {
|
||||||
|
// const { item: itemStack, entity } = event;
|
||||||
|
// if (!entity.isPlayer()) return;
|
||||||
|
// const player = Utils.server.getPlayer(entity.stringUuid);
|
||||||
|
// if (player === undefined || player === null) return;
|
||||||
|
|
||||||
|
// const item = itemStack.getItem();
|
||||||
|
// const itemsCooldowns = player.getCooldowns();
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// shouldApplyItemCooldown(player) &&
|
||||||
|
// !itemsCooldowns.isOnCooldown(item)
|
||||||
|
// ) {
|
||||||
|
// itemsCooldowns.addCooldown(item, config.cooldownSecs);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register command system
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function registerCommands() {
|
||||||
|
/**
|
||||||
|
* @param {Internal.ServerCommandEvent} event
|
||||||
|
*/
|
||||||
|
ServerEvents.commandRegistry((event) => {
|
||||||
|
const { commands, arguments: Arguments } = event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const statusCommand = (ctx) => {
|
||||||
|
const source = ctx.source;
|
||||||
|
source.sendSuccess("§6[AreaControl] Current Status:", false);
|
||||||
|
source.sendSuccess(`§e- Enabled: ${String(config.enabled)}`, false);
|
||||||
|
source.sendSuccess(
|
||||||
|
`§e- Center: (${String(config.center.x)}, ${String(config.center.y)}, ${String(config.center.z)})`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
source.sendSuccess(`§e- Radius: ${String(config.radius)}`, false);
|
||||||
|
source.sendSuccess(`§e- Mode: ${config.mode}`, false);
|
||||||
|
source.sendSuccess(
|
||||||
|
`§e- Whitelist: ${String(config.whitelist.length)} players`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
source.sendSuccess(
|
||||||
|
`§e- Cooldown: ${String(config.cooldownSecs)} Ticks (${String(config.cooldownSecs / SECOND_TICKS)}s)`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
source.sendSuccess(
|
||||||
|
`§e- Active players: ${String(Object.keys(playerStates).length)}`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const toggleCommand = (ctx) => {
|
||||||
|
config.enabled = !config.enabled;
|
||||||
|
saveConfiguration();
|
||||||
|
ctx.source.sendSuccess(
|
||||||
|
config.enabled
|
||||||
|
? "§6[AreaControl] §aEnabled"
|
||||||
|
: "§6[AreaControl] §cDisabled",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const setCenterCommand = (ctx) => {
|
||||||
|
const source = ctx.source;
|
||||||
|
if (!source.player) {
|
||||||
|
source.sendFailure("§cThis command must be run by a player");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const pos = source.player.blockPosition();
|
||||||
|
config.center = { x: pos.x, y: pos.y, z: pos.z };
|
||||||
|
updateBounds(config.center, config.radius);
|
||||||
|
saveConfiguration();
|
||||||
|
source.sendSuccess(
|
||||||
|
`§6[AreaControl] §eCenter set to (${String(pos.x)}, ${String(pos.y)}, ${String(pos.z)})`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const setRadiusCommand = (ctx) => {
|
||||||
|
const radius = Arguments.INTEGER.getResult(ctx, "radius");
|
||||||
|
if (radius < 1 || radius > 1000) {
|
||||||
|
ctx.source.sendFailure("§cRadius must be between 1 and 1000");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
config.radius = radius;
|
||||||
|
updateBounds(config.center, config.radius);
|
||||||
|
saveConfiguration();
|
||||||
|
ctx.source.sendSuccess(
|
||||||
|
`§6[AreaControl] §eRadius set to ${String(radius)}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const setModeCommand = (ctx) => {
|
||||||
|
const mode = Arguments.STRING.getResult(ctx, "mode");
|
||||||
|
if (mode !== "adventure" && mode !== "spectator") {
|
||||||
|
ctx.source.sendFailure(
|
||||||
|
'§cMode must be either "adventure" or "spectator"',
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
config.mode = mode;
|
||||||
|
saveConfiguration();
|
||||||
|
ctx.source.sendSuccess(
|
||||||
|
`§6[AreaControl] §eArea mode set to ${mode}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const setCooldownCommand = (ctx) => {
|
||||||
|
const cooldown = Arguments.INTEGER.getResult(ctx, "cooldown");
|
||||||
|
if (cooldown < 0) {
|
||||||
|
ctx.source.sendFailure(
|
||||||
|
"§cCooldown must be a non-negative number",
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
config.cooldownSecs = cooldown;
|
||||||
|
saveConfiguration();
|
||||||
|
ctx.source.sendSuccess(
|
||||||
|
`§6[AreaControl] §eItem cooldown set to ${String(cooldown)} ticks (${String(cooldown / 20)}s)`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const whitelistAddCommand = (ctx) => {
|
||||||
|
const playerName = Arguments.STRING.getResult(ctx, "player");
|
||||||
|
if (config.whitelist.indexOf(playerName) === -1) {
|
||||||
|
config.whitelist.push(playerName);
|
||||||
|
saveConfiguration();
|
||||||
|
ctx.source.sendSuccess(
|
||||||
|
`§6[AreaControl] §eAdded ${playerName} to whitelist`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ctx.source.sendFailure(
|
||||||
|
`§c${playerName} is already whitelisted`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const whitelistRemoveCommand = (ctx) => {
|
||||||
|
const playerName = Arguments.STRING.getResult(ctx, "player");
|
||||||
|
const index = config.whitelist.indexOf(playerName);
|
||||||
|
if (index !== -1) {
|
||||||
|
config.whitelist.splice(index, 1);
|
||||||
|
// Clean up player state if they're removed
|
||||||
|
const server = ctx.source.server;
|
||||||
|
const onlinePlayer = server.players.find(
|
||||||
|
/**
|
||||||
|
* @param {Internal.Player} p
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
(p) => p.username === playerName,
|
||||||
|
);
|
||||||
|
if (onlinePlayer) {
|
||||||
|
delete playerStates[onlinePlayer.stringUUID];
|
||||||
|
delete playerCooldowns[onlinePlayer.stringUUID];
|
||||||
|
}
|
||||||
|
saveConfiguration();
|
||||||
|
ctx.source.sendSuccess(
|
||||||
|
`§6[AreaControl] §eRemoved ${playerName} from whitelist`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ctx.source.sendFailure(`§c${playerName} is not whitelisted`);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const whitelistListCommand = (ctx) => {
|
||||||
|
const source = ctx.source;
|
||||||
|
if (config.whitelist.length === 0) {
|
||||||
|
source.sendSuccess(
|
||||||
|
"§6[AreaControl] §eWhitelist is empty",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
source.sendSuccess(
|
||||||
|
"§6[AreaControl] §eWhitelisted players:",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
config.whitelist.forEach((playerName) => {
|
||||||
|
source.sendSuccess(`§e- ${playerName}`, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const reloadCommand = (ctx) => {
|
||||||
|
loadConfiguration();
|
||||||
|
ctx.source.sendSuccess(
|
||||||
|
"§6[AreaControl] §aConfiguration reloaded",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} ctx
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const helpCommand = (ctx) => {
|
||||||
|
const source = ctx.source;
|
||||||
|
source.sendFailure("§cAvailable commands:");
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol status - Show current configuration",
|
||||||
|
);
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol toggle - Enable/disable the system",
|
||||||
|
);
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol setcenter - Set area center to current position",
|
||||||
|
);
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol setradius <radius> - Set area radius",
|
||||||
|
);
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol setmode <adventure|spectator> - Set area game mode",
|
||||||
|
);
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol setcooldown <ticks> - Set item cooldown",
|
||||||
|
);
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol whitelist <add|remove|list> [player] - Manage whitelist",
|
||||||
|
);
|
||||||
|
source.sendFailure(
|
||||||
|
"§e- /areacontrol reload - Reload configuration",
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the main command with all subcommands
|
||||||
|
event.register(
|
||||||
|
commands
|
||||||
|
.literal("areacontrol")
|
||||||
|
.requires((source) => source.hasPermission(2))
|
||||||
|
.executes(statusCommand) // Default to status when no args
|
||||||
|
.then(commands.literal("status").executes(statusCommand))
|
||||||
|
.then(commands.literal("toggle").executes(toggleCommand))
|
||||||
|
.then(commands.literal("setcenter").executes(setCenterCommand))
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.literal("setradius")
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.argument(
|
||||||
|
"radius",
|
||||||
|
Arguments.INTEGER.create(event),
|
||||||
|
)
|
||||||
|
.executes(setRadiusCommand),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.literal("setmode")
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.argument(
|
||||||
|
"mode",
|
||||||
|
Arguments.STRING.create(event),
|
||||||
|
)
|
||||||
|
.executes(setModeCommand),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.literal("setcooldown")
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.argument(
|
||||||
|
"cooldown",
|
||||||
|
Arguments.INTEGER.create(event),
|
||||||
|
)
|
||||||
|
.executes(setCooldownCommand),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.literal("whitelist")
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.literal("add")
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.argument(
|
||||||
|
"player",
|
||||||
|
Arguments.STRING.create(event),
|
||||||
|
)
|
||||||
|
.executes(whitelistAddCommand),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.literal("remove")
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.argument(
|
||||||
|
"player",
|
||||||
|
Arguments.STRING.create(event),
|
||||||
|
)
|
||||||
|
.executes(whitelistRemoveCommand),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
commands
|
||||||
|
.literal("list")
|
||||||
|
.executes(whitelistListCommand),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(commands.literal("reload").executes(reloadCommand))
|
||||||
|
.then(commands.literal("help").executes(helpCommand)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== INITIALIZATION ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the AreaControl system
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function initializeAreaControl() {
|
||||||
|
console.log("[AreaControl] Initializing area control system...");
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
loadConfiguration();
|
||||||
|
|
||||||
|
// Register event handlers
|
||||||
|
registerEventHandlers();
|
||||||
|
|
||||||
|
// Register commands
|
||||||
|
registerCommands();
|
||||||
|
|
||||||
|
console.log("[AreaControl] System initialized successfully");
|
||||||
|
console.log(
|
||||||
|
`[AreaControl] Area: center(${String(config.center.x)}, ${String(config.center.z)}), radius: ${String(config.radius)}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[AreaControl] Mode: ${config.mode}, Enabled: ${String(config.enabled)}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[AreaControl] Whitelisted players: ${String(config.whitelist.length)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== STARTUP EXECUTION ====================
|
||||||
|
|
||||||
|
// Initialize the system when script loads
|
||||||
|
initializeAreaControl();
|
||||||
Reference in New Issue
Block a user