diff --git a/modules/catalog.json b/modules/catalog.json index 9a7d678..be3257f 100644 --- a/modules/catalog.json +++ b/modules/catalog.json @@ -91,15 +91,15 @@ "entry": { "kind": "sapi", "path": "modules/packages/afk/sapi/src/index.ts" } }, { - "id": "feature-clean", - "configKey": "clean", - "name": "清理", + "id": "feature-area", + "configKey": "area", + "name": "区域功能", "type": "feature", - "description": "地面掉落物清理", + "description": "区域功能聚合:和平/飞行/生存/创造/清理五合一(peace/fly/survival/creative/clean)", "enabledByDefault": false, "canDisable": true, "requires": [], - "entry": { "kind": "sapi", "path": "modules/packages/clean/sapi/src/index.ts" } + "entry": { "kind": "sapi", "path": "modules/packages/feature-area/sapi/src/index.ts" } }, { "id": "feature-tps", @@ -178,50 +178,6 @@ "requires": [], "entry": { "kind": "sapi", "path": "modules/packages/inventory-switcher/sapi/src/index.ts" } }, - { - "id": "feature-area-fly", - "configKey": "fly", - "name": "区域飞行", - "type": "feature", - "description": "定义区域内允许飞行", - "enabledByDefault": false, - "canDisable": true, - "requires": [], - "entry": { "kind": "sapi", "path": "modules/packages/fly/sapi/src/index.ts" } - }, - { - "id": "feature-area-creative", - "configKey": "creative", - "name": "创造区域", - "type": "feature", - "description": "进入区域自动切换创造模式", - "enabledByDefault": false, - "canDisable": true, - "requires": [], - "entry": { "kind": "sapi", "path": "modules/packages/creative/sapi/src/index.ts" } - }, - { - "id": "feature-area-survival", - "configKey": "survival", - "name": "生存区域", - "type": "feature", - "description": "进入区域自动切换生存模式", - "enabledByDefault": false, - "canDisable": true, - "requires": [], - "entry": { "kind": "sapi", "path": "modules/packages/survival/sapi/src/index.ts" } - }, - { - "id": "feature-area-peace", - "configKey": "peace", - "name": "和平区域", - "type": "feature", - "description": "定义区域内禁止刷怪", - "enabledByDefault": false, - "canDisable": true, - "requires": [], - "entry": { "kind": "sapi", "path": "modules/packages/peace/sapi/src/index.ts" } - }, { "id": "feature-qa", "configKey": "qa", diff --git a/modules/module-lock.json b/modules/module-lock.json index edb540e..15472cb 100644 --- a/modules/module-lock.json +++ b/modules/module-lock.json @@ -25,7 +25,7 @@ "enabled": true, "updatedAt": 1784487209308 }, - "feature-clean": { + "feature-area": { "enabled": true, "updatedAt": 1784487209308 }, @@ -53,22 +53,6 @@ "enabled": true, "updatedAt": 1784041411927 }, - "feature-area-fly": { - "enabled": true, - "updatedAt": 1784463641711 - }, - "feature-area-creative": { - "enabled": true, - "updatedAt": 1784041413141 - }, - "feature-area-survival": { - "enabled": true, - "updatedAt": 1784041413608 - }, - "feature-area-peace": { - "enabled": true, - "updatedAt": 1784041414102 - }, "feature-qa": { "enabled": true, "updatedAt": 1784463641711 diff --git a/modules/packages/clean/sapi/manifest.json b/modules/packages/clean/sapi/manifest.json deleted file mode 100644 index d3ef82b..0000000 --- a/modules/packages/clean/sapi/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "handlers": [], - "routes": [], - "migrations": [], - "notes": "clean: no SAPI→db-server HTTP calls; pure game-side logic" -} diff --git a/modules/packages/clean/sapi/src/Clean.ts b/modules/packages/clean/sapi/src/Clean.ts deleted file mode 100644 index cad6757..0000000 --- a/modules/packages/clean/sapi/src/Clean.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* ---------------------------------------- *\ - * Name : DogeLake Cleaner * - * Description : 清理垃圾 集中到固定地点 * - * Version : 1.1.0 * - * Author : ENIAC_Jushi * -\* ---------------------------------------- */ - -import { BlockComponentTypes, Entity, ItemStack, system, world } from "@minecraft/server"; -import { Command } from "@sfmc/sdk/sapi/runtime"; -import { ConfigManager } from "@sfmc/sdk/module-loader"; -import { debug } from "@sfmc/sdk/sapi/runtime"; -import { Permission } from "@sfmc/sdk/sapi/runtime"; -import * as Tool from "@sfmc/sdk/sapi/runtime"; - -/** - * [重要] - * 人工准备:放置箱子、设置常加载区域 - * 清理步骤:垃圾清理到固定地点的一排箱子内,一个箱子装满则放入下一个箱子,箱子上有牌子,记录垃圾进入箱子的时段,若所有箱子均已装满,则从头开始 - */ -export class Clean { - static _instance: Clean | undefined = undefined; - static getInstance() { - if (!Clean._instance) { - this._instance = new Clean(); - } - return this._instance; - } - - private static cleanIndex = 0; - - startPoint: [number, number, number] = [0, 0, 0]; - size: [number, number] = [5, 5]; - direction = -1; // 箱子的朝向 - killList: string[] = []; - face = -1; - intervalId: number | undefined = undefined; - itemMax = 128; - timeout = 60; - - init() { - debug.i("CLEAN", "Clean.init"); - // 初始化设置 - const cleanCfg = ConfigManager.getClean(); - const recycleBin = ConfigManager.getGrid("clean_recycle_bin"); - if (recycleBin) { - this.startPoint = [recycleBin.start[0], recycleBin.start[1], recycleBin.start[2]]; - this.size = [recycleBin.size[0], recycleBin.size[1]]; - this.direction = recycleBin.direction; - this.face = recycleBin.face; - } - this.killList = JSON.parse(ConfigManager.getSetting("clean_kill_list", "[]")); - this.itemMax = cleanCfg.itemMax; - this.timeout = cleanCfg.pollInterval; - - this.startCleanInterval(); - } - - getCleanIndex() { - return Clean.cleanIndex; - } - - setCleanIndex(index: number) { - Clean.cleanIndex = index; - } - - /** - * 将物品放入箱子 - * @param itemProvider 物品给予函数,函数会返回物品的ItemStack,当返回undefined时说明任务结束此时会退出 - * @param isFirstCall 是否是首次调用,如果是,在一次循环后物品没有放完,会重置index,再进行一次循环直到放完 - */ - placeItem(itemProvider: () => ItemStack | undefined, isFirstCall: boolean = true) { - // 确定面的增长方向 - let base = Tool.getBase(this.direction); - - // 确定箱子朝向 - let cardinalDirection = Tool.getChestCardinal(this.direction, this.face); - let facingDirection = Tool.getSignFacing(this.direction, this.face); - - let index = 0; - let currentIndex = this.getCleanIndex(); // 当前箱子的索引,仅在跳过阶段使用 - const dimension = world.getDimension("overworld"); - for (let mainAxis = 0; mainAxis < this.size[0]; mainAxis++) { - for (let y = 0; y < this.size[1]; y++) { - // 若还未到达当前空箱子的索引,则跳过 - index++; - if (index < currentIndex) { - continue; - } - // 获取坐标的方块 - let coordinate = { - x: this.startPoint[0] + mainAxis * base[0] * 2, - y: this.startPoint[1] + y, - z: this.startPoint[2] + mainAxis * base[1] * 2, - }; // 主箱子 - let coordinate2 = { - x: coordinate.x + base[0], - y: coordinate.y, - z: coordinate.z + base[1], - }; // 主箱子右边一个箱子 - // 获取方块 - let block = dimension.getBlock(coordinate); - // 方块应该是箱子 不是则放置 - Tool.ensureDoubleChest(dimension, coordinate, cardinalDirection, this.direction); - - // 获取主箱子的容器(可以获取到连体的容器) - let inventory = block!.getComponent(BlockComponentTypes.Inventory) as any; - if (!inventory || !inventory.container) { - continue; - } - let container = inventory.container; - if (container.emptySlotsCount === 0) { - container.clearAll(); - } - - // 放入物品 - while (container.emptySlotsCount > 0) { - let item = itemProvider(); - if (!item) { - // 没有物品需要处理了,结束任务 - return; - } - container.addItem(item); - } - // 当前的箱子装满了,更新索引为下一个箱子 - this.setCleanIndex(index + 1); - // 放置告示牌 - let signCoordinate = { - x: coordinate2.x + (base[0] !== 0 ? 0 : this.face), - y: coordinate2.y, - z: coordinate2.z + (base[1] !== 0 ? 0 : this.face), - }; - Tool.placeSign(dimension, signCoordinate, facingDirection, this.getTimeStr()); - } - } - // 一轮循环后,任务仍然没有结束,归零,进行新一轮循环 - this.setCleanIndex(0); - if (isFirstCall) { - this.placeItem(itemProvider, false); - } - } - - /** - * 开始清理 - */ - startClean(entities?: Entity[]) { - debug.i("CLEAN", `startClean: entityCount=${entities?.length || "all"}`); - // 获取所有物品实体 - let itemEntities = entities ?? this.getAllItemEntities(); - - this.placeItem(() => { - while (itemEntities.length > 0) { - let itemEntity = itemEntities.pop()!; - let stack = (itemEntity.getComponent("minecraft:item") as any).itemStack; - if (!stack) { - continue; - } - // 在直接清除列表中的物品实体 直接清除 - if (this.killList.some((value) => value === stack.typeId)) { - itemEntity.kill(); - continue; - } - // 清除物品实体并返回物品 - itemEntity.kill(); - return stack; - } - return undefined; - }); - } - - startCleanInterval() { - debug.i("CLEAN", "startCleanInterval"); - if (this.intervalId) { - system.clearRun(this.intervalId); - this.intervalId = undefined; - } - // 扫描 - this.intervalId = system.runInterval(() => { - let entities = this.getAllItemEntities(); - if (entities.length > this.itemMax) { - world.sendMessage({ rawtext: [{ text: "「§6読経するヤマビコ ~ 幽谷 響子§f」 距离清理掉落物还有§c 5 §fs" }] }); - system.runTimeout(() => { - this.startClean(undefined); - system.runTimeout(() => { - world.sendMessage({ rawtext: [{ text: "§a* 已清理掉落物 *" }] }); - }, 5); - }, 100); - } - }, this.timeout * 20); - } - - stopCleanInterval() { - debug.i("CLEAN", "stopCleanInterval"); - if (this.intervalId) { - system.clearRun(this.intervalId); - this.intervalId = undefined; - } - } - - stop() { - debug.i("CLEAN", "Clean.stop"); - this.stopCleanInterval(); - } - - /** - * 获取世界的所有物品 - */ - getAllItemEntities() { - let itemEntities = world.getDimension("overworld").getEntities({ type: "item" }); - itemEntities.push(...world.getDimension("nether").getEntities({ type: "item" })); - itemEntities.push(...world.getDimension("the_end").getEntities({ type: "item" })); - return itemEntities; - } - - getTimeStr() { - const { date, time } = Tool.getShanghaiTime(); - return `\n${date}\n${time}`; - } -} - -export function registerCommand(): void { - Permission.register("clean.admin", Permission.OP); - Command.register( - "clean", - "clean.admin", - () => { - Clean.getInstance()!.startClean(undefined); - }, - "开始扫地", - "clean" - ); -} diff --git a/modules/packages/clean/sapi/src/index.ts b/modules/packages/clean/sapi/src/index.ts deleted file mode 100644 index 570d8aa..0000000 --- a/modules/packages/clean/sapi/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @sfmc/module-clean — SAPI 侧入口 - * - * 暴露给行为包启动期 (ModuleRegistry.register 调用点): - * Clean 类 + registerCommand 函数 - */ -export { Clean, registerCommand } from "./Clean.js"; diff --git a/modules/packages/creative/package.json b/modules/packages/creative/package.json deleted file mode 100644 index 3f00dae..0000000 --- a/modules/packages/creative/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@sfmc/module-creative", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "SAPI module: creative 模块。SAPI 侧入口,sapi/src/index.ts。", - "main": "sapi/src/index.ts", - "scripts": { - "typecheck": "tsc --noEmit -p sapi/tsconfig.json" - }, - "dependencies": { - "@sfmc/sdk": "*" - }, - "peerDependencies": { - "@minecraft/server": "2.10.0-beta.1.26.40-preview.30" - } -} diff --git a/modules/packages/creative/sapi/manifest.json b/modules/packages/creative/sapi/manifest.json deleted file mode 100644 index 549d6ae..0000000 --- a/modules/packages/creative/sapi/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "handlers": [], - "routes": [], - "migrations": [], - "notes": "creative: no SAPI→db-server HTTP calls; pure game-side logic" -} diff --git a/modules/packages/creative/sapi/src/CreativeArea.ts b/modules/packages/creative/sapi/src/CreativeArea.ts deleted file mode 100644 index 3650335..0000000 --- a/modules/packages/creative/sapi/src/CreativeArea.ts +++ /dev/null @@ -1,370 +0,0 @@ -/* ---------------------------------------- *\ - * Name : 区域创造 * - * Description : 进出指定区域切换创造/生存模式 * - * Version : 1.0.0 * - * Author : Shiroha7z * -\* ---------------------------------------- */ - -import { - Entity, - EntityInitializationCause, - EntitySpawnAfterEvent, - GameMode, - Player, - PlayerBreakBlockBeforeEvent, - PlayerDimensionChangeAfterEvent, - PlayerPlaceBlockBeforeEvent, - PlayerSpawnAfterEvent, - system, - world, -} from "@minecraft/server"; -import { Permission } from "@sfmc/sdk/sapi/runtime"; -import { ConfigManager } from "@sfmc/sdk/module-loader"; -import * as Tool from "@sfmc/sdk/sapi/runtime"; - -export class CreativeArea { - static _instance: CreativeArea; - static getInstance() { - if (!CreativeArea._instance) { - CreativeArea._instance = new CreativeArea(); - } - return CreativeArea._instance; - } - - /** 连锁开关(同时控制 CreativeArea + SurvivalArea) */ - static enable = true; - - private readonly BORDER_THRESHOLD = 10; - private readonly BUFFER_ZONE = 3; - - private subscriptions: Array = []; - private tickRunIds: number[] = []; - - /** 注册命令和权限(由 entry.ts 在 startup 阶段调用) */ - registerCommandsAndPermissions() { - Permission.register("creativearea.place_banned", Permission.Admin); - } - - /** 注册事件(由 entry.ts 统一调用) */ - registerEvents() { - if (this.subscriptions.length > 0) return; - this.subscriptions.push( - world.afterEvents.playerSpawn.subscribe((event: PlayerSpawnAfterEvent) => { - if (!event.initialSpawn) return; - system.runTimeout(() => { - const areaName = this.inArea(event.player); - if (areaName !== undefined) { - this.enterArea(event.player, areaName); - } else if ( - event.player.getGameMode() === GameMode.Creative || - event.player.getGameMode() === GameMode.Spectator - ) { - event.player.setGameMode(GameMode.Survival); - } - }, 60); - }) - ); - - this.subscriptions.push( - world.afterEvents.playerDimensionChange.subscribe((event: PlayerDimensionChangeAfterEvent) => { - if (!CreativeArea.enable) return; - system.runTimeout(() => { - const areaName = this.inArea(event.player); - const currentArea = event.player.getDynamicProperty("hpbe:creative_area") as string | undefined; - if (currentArea === undefined && areaName !== undefined) { - this.enterArea(event.player, areaName); - } else if (currentArea !== undefined && areaName === undefined) { - this.leaveArea(event.player, currentArea); - } - }, 10); - }) - ); - - this.subscriptions.push( - world.afterEvents.entitySpawn.subscribe((event: EntitySpawnAfterEvent) => { - if (!CreativeArea.enable) return; - if (!event.entity) return; - if (event.entity.typeId === "minecraft:player") return; - if (!this.creativeDims.has(event.entity.dimension.id)) return; - try { - if (event.cause === EntityInitializationCause.Spawned) { - if (this.inArea(event.entity) !== undefined || this.inBufferZone(event.entity)) { - event.entity.remove(); - } - } - } catch {} - }) - ); - - this.subscriptions.push( - world.beforeEvents.playerPlaceBlock.subscribe((event: PlayerPlaceBlockBeforeEvent) => { - if (!CreativeArea.enable) return; - const player = event.player; - if (player.getGameMode() !== GameMode.Creative) return; - if (!this.inAreaByPos(event.block.location.x, event.block.location.z, player.dimension.id)) { - event.cancel = true; - Tool.Msg.error(`你只能在创造区域内放置方块。`, player); - return; - } - if (ConfigManager.getBannedItems().indexOf(event.permutationToPlace.type.id) !== -1) { - if (!Permission.check(player, "creativearea.place_banned")) { - event.cancel = true; - Tool.Msg.error(`创造区域内禁止放置 ${event.permutationToPlace.type.id}。`, player); - } - } - }) - ); - - this.subscriptions.push( - world.beforeEvents.playerBreakBlock.subscribe((event: PlayerBreakBlockBeforeEvent) => { - if (!CreativeArea.enable) return; - if (event.player.getGameMode() !== GameMode.Creative) return; - if (!this.inAreaByPos(event.block.location.x, event.block.location.z, event.player.dimension.id)) { - event.cancel = true; - Tool.Msg.error(`你只能破坏创造区域内的方块。`, event.player); - } - }) - ); - } - - cleanup() { - for (const s of this.subscriptions) { - try { - s.unsubscribe(); - } catch {} - } - this.subscriptions = []; - for (const id of this.tickRunIds) { - try { - system.clearRun(id); - } catch {} - } - this.tickRunIds = []; - } - - init() { - this.startTick(); - this.startBorderFastCheck(); - //this.startBorderWarning(); - } - - // ========================================== - // 区域判定 - // ========================================== - - private inArea(entity: Entity): string | undefined { - for (const area of ConfigManager.getAreas("creative")) { - if (entity.dimension.id === area.dimension) { - if ( - Tool.pointInArea_2D( - entity.location.x, - entity.location.z, - area.start[0], - area.start[1], - area.end[0], - area.end[1] - ) - ) { - return area.name; - } - } - } - return undefined; - } - - private inAreaByPos(x: number, z: number, dimensionId: string): boolean { - for (const area of ConfigManager.getAreas("creative")) { - if (dimensionId === area.dimension) { - if (Tool.pointInArea_2D(x, z, area.start[0], area.start[1], area.end[0], area.end[1])) { - return true; - } - } - } - return false; - } - - private isNearBorder(entity: Entity, threshold: number = this.BORDER_THRESHOLD): boolean { - for (const area of ConfigManager.getAreas("creative")) { - if (entity.dimension.id !== area.dimension) continue; - const minX = Math.min(area.start[0], area.end[0]) - threshold; - const maxX = Math.max(area.start[0], area.end[0]) + threshold; - const minZ = Math.min(area.start[1], area.end[1]) - threshold; - const maxZ = Math.max(area.start[1], area.end[1]) + threshold; - if ( - entity.location.x >= minX && - entity.location.x <= maxX && - entity.location.z >= minZ && - entity.location.z <= maxZ - ) - return true; - } - return false; - } - - private inBufferZone(entity: Entity): boolean { - for (const area of ConfigManager.getAreas("creative")) { - if (entity.dimension.id !== area.dimension) continue; - const minX = Math.min(area.start[0], area.end[0]); - const maxX = Math.max(area.start[0], area.end[0]); - const minZ = Math.min(area.start[1], area.end[1]); - const maxZ = Math.max(area.start[1], area.end[1]); - const x = entity.location.x, - z = entity.location.z; - const inExpanded = - x >= minX - this.BUFFER_ZONE && - x <= maxX + this.BUFFER_ZONE && - z >= minZ - this.BUFFER_ZONE && - z <= maxZ + this.BUFFER_ZONE; - if (!inExpanded) continue; - if (x >= minX && x <= maxX && z >= minZ && z <= maxZ) continue; - return true; - } - return false; - } - - private get creativeDims(): Set { - const dims = new Set(); - for (const area of ConfigManager.getAreas("creative")) dims.add(area.dimension); - return dims; - } - - // ========================================== - // 进入 / 离开 处理(背包由 InventorySwitcher 接管) - // ========================================== - - private enterArea(player: Player, areaName: string) { - this.saveScores(player); - player.setGameMode(GameMode.Creative); - player.setDynamicProperty("hpbe:creative_area", areaName); - Tool.Msg.info(`进入 §a${areaName}创造区域§r ,切换为创造模式。`, player); - } - - private leaveArea(player: Player, areaName: string) { - this.restoreScores(player); - player.setGameMode(GameMode.Survival); - player.setDynamicProperty("hpbe:creative_area", undefined); - Tool.Msg.info(`离开 §a${areaName}创造区域§r ,恢复生存模式。`, player); - } - - // ========================================== - // 计分项保存 / 恢复 - // ========================================== - - private saveScores(player: Player) { - const identity = player.scoreboardIdentity; - if (!identity) return; - const scores: Record = {}; - for (const obj of world.scoreboard.getObjectives()) { - try { - const score = obj.getScore(identity); - if (score !== undefined) scores[obj.id] = score; - } catch {} - } - if (Object.keys(scores).length > 0) { - player.setDynamicProperty("hpbe:creative_scores", JSON.stringify(scores)); - } - } - - private restoreScores(player: Player) { - const raw = player.getDynamicProperty("hpbe:creative_scores") as string | undefined; - const scores: Record | undefined = raw ? JSON.parse(raw) : undefined; - if (!scores) return; - const identity = player.scoreboardIdentity; - if (!identity) return; - for (const obj of world.scoreboard.getObjectives()) { - const v = scores[obj.id]; - if (v !== undefined) { - try { - obj.setScore(identity, v); - } catch {} - } - } - player.setDynamicProperty("hpbe:creative_scores", undefined); - } - - // ========================================== - // 定时扫描(进出检测) - // ========================================== - - private startTick() { - this.tickRunIds.push( - system.runInterval(() => { - if (!CreativeArea.enable) return; - for (const player of world.getPlayers()) { - if (player.getGameMode() === GameMode.Spectator) continue; - const currentArea = player.getDynamicProperty("hpbe:creative_area") as string | undefined; - if (currentArea === undefined) { - const areaName = this.inArea(player); - if (areaName !== undefined) this.enterArea(player, areaName); - } else { - if (this.inArea(player) === undefined) this.leaveArea(player, currentArea); - } - } - }, 10) - ); - } - - // ========================================== - // 边界快速检测 - // ========================================== - - private startBorderFastCheck() { - this.tickRunIds.push( - system.runInterval(() => { - if (!CreativeArea.enable) return; - for (const player of world.getPlayers()) { - if (player.getGameMode() !== GameMode.Creative) continue; - if (!this.isNearBorder(player)) continue; - const currentArea = player.getDynamicProperty("hpbe:creative_area") as string | undefined; - if (currentArea !== undefined && this.inArea(player) === undefined) { - this.leaveArea(player, currentArea); - } - } - }, 2) - ); - } - - // ========================================== - // 边界视觉警告 - // ========================================== - - /* startBorderWarning disabled — particle API churn across BDS versions; kept for future re-enable. - private startBorderWarning() { - this.tickRunIds.push( - system.runInterval(() => { - if (!CreativeArea.enable) return; - for (const player of world.getPlayers()) { - for (const area of ConfigManager.getAreas("creative")) { - if (player.dimension.id !== area.dimension) continue; - const pos = player.location; - const minX = Math.min(area.start[0], area.end[0]); - const maxX = Math.max(area.start[0], area.end[0]); - const minZ = Math.min(area.start[1], area.end[1]); - const maxZ = Math.max(area.start[1], area.end[1]); - const d = this.BORDER_WARNING_DISTANCE; - if (pos.x < minX - d || pos.x > maxX + d || pos.z < minZ - d || pos.z > maxZ + d) continue; - - const cx = Math.max(minX, Math.min(maxX, pos.x)); - const cz = Math.max(minZ, Math.min(maxZ, pos.z)); - let bx = cx, - bz = cz; - if (cx === pos.x && cz === pos.z) { - const dx = Math.min(pos.x - minX, maxX - pos.x); - const dz = Math.min(pos.z - minZ, maxZ - pos.z); - if (dx < dz) bx = pos.x - minX < maxX - pos.x ? minX : maxX; - else bz = pos.z - minZ < maxZ - pos.z ? minZ : maxZ; - } - const y = Math.floor(pos.y); - try { - for (let dy = -1; dy <= 2; dy++) { - player.dimension.spawnParticle("minecraft:colored_flame_particle", { x: bx, y: y + dy, z: bz }); - } - } catch {} - break; - } - } - }, 20) - ); - } - */ -} \ No newline at end of file diff --git a/modules/packages/creative/sapi/src/index.ts b/modules/packages/creative/sapi/src/index.ts deleted file mode 100644 index a28ab0c..0000000 --- a/modules/packages/creative/sapi/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @sfmc/module-creative — SAPI 侧入口 - * - * 暴露给行为包启动期 (ModuleRegistry.register 调用点): - * - CreativeArea.getInstance():注册命令/权限、订阅事件、生命周期 - * - * 跨包依赖的相对路径保留 — 仓内 BP 暂未拆出,Stage D-G 把 gui/libs/api 等 - * 迁到各自模块后再修。 - */ - -export { CreativeArea } from "./CreativeArea.js"; \ No newline at end of file diff --git a/modules/packages/creative/sapi/tsconfig.json b/modules/packages/creative/sapi/tsconfig.json deleted file mode 100644 index 85c1345..0000000 --- a/modules/packages/creative/sapi/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../sdk/@sfmc-sdk/tsconfig.json", - "compilerOptions": { - "noEmit": true, - "rootDir": "../../../.." - }, - "include": ["src/**/*"] -} diff --git a/modules/packages/feature-area/configs-default/area.json b/modules/packages/feature-area/configs-default/area.json new file mode 100644 index 0000000..e4428a1 --- /dev/null +++ b/modules/packages/feature-area/configs-default/area.json @@ -0,0 +1,40 @@ +{ + "areas": [ + { + "name": "spawn_peace", + "dimension": "minecraft:overworld", + "start": [0, 0], + "end": [100, 100], + "features": { + "peace": true, + "fly": true, + "survival": false, + "creative": false, + "clean": false + } + } + ], + "scan_interval_ticks": 40, + "peace": { + "filters": [ + { "family": "monster", "exclude_family": "zombie_villager" }, + { "family": "monster", "exclude_family": "wither" }, + { "family": "monster", "exclude_family": "illager" } + ] + }, + "creative": { + "banned_items": [] + }, + "clean": { + "item_max": 192, + "poll_interval": 60, + "kill_list": [], + "recycle_bin": { + "start": [0, 60, 0], + "size": [5, 5], + "direction": -1, + "face": -1 + } + }, + "notes": "五合一区域模块统一配置。areas[].features. 布尔位控制每个区域启用哪些子功能(peace/fly/survival/creative/clean);某功能在任意区域被开启即视为激活。start/end 为水平面 [x, z] 矩形。scan_interval_ticks 为 fly 区域进出扫描周期。peace.filters 为和平区怪物过滤器(family + exclude_family)。creative.banned_items 为创造区禁放物品 typeId。clean 块为掉落物清理配置(阈值/周期/直清列表/回收箱阵列几何);clean 属全服语义,非区域范围,仅用 features.clean 作为总开关。" +} diff --git a/modules/packages/clean/package.json b/modules/packages/feature-area/package.json similarity index 62% rename from modules/packages/clean/package.json rename to modules/packages/feature-area/package.json index 2268e1d..6093361 100644 --- a/modules/packages/clean/package.json +++ b/modules/packages/feature-area/package.json @@ -1,9 +1,9 @@ { - "name": "@sfmc/module-clean", + "name": "@sfmc/module-feature-area", "version": "0.1.0", "private": true, "type": "module", - "description": "SAPI module: DogeLake Cleaner - 清理掉落物并集中入箱。", + "description": "SAPI module: 区域功能聚合模块(peace/fly/survival/creative/clean 五合一)。SAPI 侧入口 sapi/src/index.ts。", "main": "sapi/src/index.ts", "scripts": { "typecheck": "tsc --noEmit -p sapi/tsconfig.json" diff --git a/modules/packages/peace/resource_pack/entities/peace_skeleton.json b/modules/packages/feature-area/resource_pack/entities/peace_skeleton.json similarity index 100% rename from modules/packages/peace/resource_pack/entities/peace_skeleton.json rename to modules/packages/feature-area/resource_pack/entities/peace_skeleton.json diff --git a/modules/packages/peace/resource_pack/entities/peace_spider.json b/modules/packages/feature-area/resource_pack/entities/peace_spider.json similarity index 100% rename from modules/packages/peace/resource_pack/entities/peace_spider.json rename to modules/packages/feature-area/resource_pack/entities/peace_spider.json diff --git a/modules/packages/peace/resource_pack/entities/peace_zombie.json b/modules/packages/feature-area/resource_pack/entities/peace_zombie.json similarity index 100% rename from modules/packages/peace/resource_pack/entities/peace_zombie.json rename to modules/packages/feature-area/resource_pack/entities/peace_zombie.json diff --git a/modules/packages/feature-area/sapi/manifest.json b/modules/packages/feature-area/sapi/manifest.json new file mode 100644 index 0000000..25a50ba --- /dev/null +++ b/modules/packages/feature-area/sapi/manifest.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 2, + "id": "feature-area", + "name": "区域功能", + "type": "feature", + "configKey": "area", + "requires": [], + + "permissions": ["config:read:area"], + + "services": { + "provides": [ + { + "name": "area.byName", + "input": { + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] + }, + "output": { "type": "object" } + }, + { + "name": "area.byPoint", + "input": { + "type": "object", + "properties": { + "dimension": { "type": "string" }, + "x": { "type": "number" }, + "z": { "type": "number" }, + "feature": { "type": "string" } + }, + "required": ["dimension", "x", "z"] + }, + "output": { "type": "object" } + } + ], + "requires": [] + }, + + "notes": "五合一区域模块:合并 v1 的 feature-area-fly / feature-area-creative / feature-area-survival / feature-area-peace / feature-clean。单一 ModuleRegistry.register,内部委派给 peace/fly/survival/creative/clean 五个子生命周期。区域定义统一走 configs/area.json 的 areas[].features. 布尔开关,通过 SDK config.get 读取(不再用 ConfigManager.getAreas)。clean 子功能因不属于区域范围语义,额外读取 area.json 的 clean 配置块(recycle_bin / kill_list / item_max / poll_interval);creative 与 survival 存在同模块内耦合(survival 依据 creative 区域判定强制生存)。" +} diff --git a/modules/packages/feature-area/sapi/src/area-service.ts b/modules/packages/feature-area/sapi/src/area-service.ts new file mode 100644 index 0000000..8286c18 --- /dev/null +++ b/modules/packages/feature-area/sapi/src/area-service.ts @@ -0,0 +1,117 @@ +/** + * area-service.ts — 区域配置加载 + 对外服务(area.byName / area.byPoint) + * + * v2 协议:配置只走 SDK config 抽屉,不再用 ConfigManager.getAreas()。 + * + * config.get 只能拿到 configs/area.json 的顶层键(SDK 客户端把顶层键逐个缓存), + * 因此这里用 config.get("areas") / config.get("scan_interval_ticks") / + * config.get("clean") 分别读取,再组装成 AreaConfig。 + * + * 区域列表在 init 时读入内存缓存(_areas),之后各子生命周期的 tick 扫描 + * 直接同步读缓存(config.get 是异步的,不能每 tick await);config.onChange + * 订阅变更以热更新缓存。 + */ + +import { config } from "@sfmc/sdk/sapi/config"; +import { pointInArea_2D } from "@sfmc/sdk/sapi/runtime"; +import type { Area, AreaConfig, CleanConfig, FeatureName } from "./area-types.js"; + +let _areas: Area[] = []; +let _scanIntervalTicks = 40; +let _clean: CleanConfig | undefined = undefined; +let _loaded = false; + +/** + * 读取 configs/area.json 并填充内存缓存。init 阶段调一次。 + * 返回组装好的 AreaConfig(等价于 config.get("area") 的语义)。 + */ +export async function initAreaConfig(): Promise { + const areas = await config.get("areas"); + const scan = await config.get("scan_interval_ticks"); + const clean = await config.get("clean"); + + _areas = Array.isArray(areas) ? areas : []; + _scanIntervalTicks = typeof scan === "number" && scan > 0 ? scan : 40; + _clean = clean ?? undefined; + _loaded = true; + + // 热更新:任意子键变更时刷新缓存 + config.onChange((key, value) => { + if (key === "areas" && Array.isArray(value)) _areas = value as Area[]; + else if (key === "scan_interval_ticks" && typeof value === "number" && value > 0) _scanIntervalTicks = value; + else if (key === "clean" && value && typeof value === "object") _clean = value as CleanConfig; + }); + + return { areas: _areas, scan_interval_ticks: _scanIntervalTicks, ...(_clean ? { clean: _clean } : {}) }; +} + +/** 是否已加载配置 */ +export function isAreaConfigLoaded(): boolean { + return _loaded; +} + +/** 扫描周期(tick) */ +export function getScanIntervalTicks(): number { + return _scanIntervalTicks; +} + +/** clean 子功能配置(可能未配置) */ +export function getCleanConfig(): CleanConfig | undefined { + return _clean; +} + +/** 全部区域(缓存快照) */ +export function allAreas(): Area[] { + return _areas; +} + +/** 返回开启了指定子功能的所有区域 */ +export function areasByFeature(feature: FeatureName): Area[] { + return _areas.filter((a) => a.features?.[feature] === true); +} + +/** 指定子功能是否在任何区域启用(子生命周期用它判断自己是否"激活") */ +export function isFeatureActive(feature: FeatureName): boolean { + return _areas.some((a) => a.features?.[feature] === true); +} + +/* ── 对外服务实现 ─────────────────────────────────────────────── */ + +/** + * service: area.byName —— 按名字查区域。 + */ +export function areaByName(name: string): Area | null { + return _areas.find((a) => a.name === name) ?? null; +} + +/** + * service: area.byPoint —— 查覆盖某水平坐标点的区域。 + * 可选 feature:只在开启该子功能的区域中匹配。 + */ +export function areaByPoint(input: { + dimension: string; + x: number; + z: number; + feature?: FeatureName; +}): Area | null { + for (const a of _areas) { + if (a.dimension !== input.dimension) continue; + if (input.feature && a.features?.[input.feature] !== true) continue; + if (pointInArea_2D(input.x, input.z, a.start[0], a.start[1], a.end[0], a.end[1])) { + return a; + } + } + return null; +} + +/** + * 子生命周期用:某坐标点是否落在"开启指定子功能"的区域内,命中返回该区域。 + */ +export function pointInFeatureArea( + feature: FeatureName, + dimension: string, + x: number, + z: number +): Area | null { + return areaByPoint({ dimension, x, z, feature }); +} diff --git a/modules/packages/feature-area/sapi/src/area-types.ts b/modules/packages/feature-area/sapi/src/area-types.ts new file mode 100644 index 0000000..e8a7571 --- /dev/null +++ b/modules/packages/feature-area/sapi/src/area-types.ts @@ -0,0 +1,58 @@ +/** + * area-types.ts — 区域配置类型定义 + * + * 统一 schema:一个 area 通过 features. 布尔开关声明它启用哪些子功能。 + * 五个子功能(peace/fly/survival/creative/clean)共享同一份 areas 列表, + * 每个子生命周期只关心 features 里属于自己的那一位。 + */ + +/** 五个子功能名 */ +export type FeatureName = "peace" | "fly" | "survival" | "creative" | "clean"; + +/** 每个区域可开启的子功能开关 */ +export interface AreaFeatures { + peace?: boolean; + fly?: boolean; + survival?: boolean; + creative?: boolean; + clean?: boolean; +} + +/** 单个区域(2D 矩形,记录维度 + 起止水平坐标 + 功能开关) */ +export interface Area { + /** 区域名(全局展示用,fly 进出提示等) */ + name: string; + /** 维度 id,如 "minecraft:overworld" */ + dimension: string; + /** 起点 [x, z] */ + start: [number, number]; + /** 终点 [x, z] */ + end: [number, number]; + /** 子功能开关 */ + features: AreaFeatures; +} + +/** clean 子功能配置(不属于区域范围语义,单独一块) */ +export interface CleanConfig { + /** 掉落物数量阈值,超过则触发清理 */ + item_max: number; + /** 扫描周期(秒) */ + poll_interval: number; + /** 直接清除(不入箱)的物品 typeId 列表 */ + kill_list: string[]; + /** 回收箱阵列几何信息 */ + recycle_bin: { + start: [number, number, number]; + size: [number, number]; + direction: number; + face: number; + }; +} + +/** configs/area.json 的整体结构 */ +export interface AreaConfig { + areas: Area[]; + scan_interval_ticks: number; + clean?: CleanConfig; + notes?: string; +} diff --git a/modules/packages/feature-area/sapi/src/index.ts b/modules/packages/feature-area/sapi/src/index.ts new file mode 100644 index 0000000..605c80b --- /dev/null +++ b/modules/packages/feature-area/sapi/src/index.ts @@ -0,0 +1,68 @@ +/** + * @sfmc/module-feature-area — v2 入口(五合一区域模块) + * + * 用一个 ModuleRegistry.register 聚合五个子生命周期: + * peace / fly / survival / creative / clean + * 分别合并自 v1 的 feature-area-peace / feature-area-fly / feature-area-survival / + * feature-area-creative / feature-clean。 + * + * 每个子功能通过统一配置 configs/area.json 的 areas[].features. 布尔位 + * 独立启用/禁用(某功能在任意区域被开启即视为激活)。区域数据统一由 + * area-service 经 SDK config 抽屉读取,不再使用 ConfigManager.getAreas()。 + * + * 对外服务(manifest services.provides): + * - area.byName —— 按名字查区域 + * - area.byPoint —— 按坐标点查区域(可按 feature 过滤) + */ + +import { ModuleRegistry, type ModuleLifecycle } from "@sfmc/sdk/module-loader"; + +import { initAreaConfig } from "./area-service.js"; +import { peaceLifecycle } from "./lifecycles/peace.js"; +import { flyLifecycle } from "./lifecycles/fly.js"; +import { survivalLifecycle } from "./lifecycles/survival.js"; +import { creativeLifecycle } from "./lifecycles/creative.js"; +import { cleanLifecycle } from "./lifecycles/clean.js"; + +const MODULE_ID = "feature-area"; + +// 子生命周期集合。顺序有意义:creative 先于 survival(survival 读取 creative 的连锁开关)。 +const subLifecycles: ModuleLifecycle[] = [ + peaceLifecycle, + flyLifecycle, + creativeLifecycle, + survivalLifecycle, + cleanLifecycle, +]; + +ModuleRegistry.register({ + id: MODULE_ID, + afterWorldLoad: false, + lifecycle: { + registerPermissions() { + for (const l of subLifecycles) l.registerPermissions?.(); + }, + + registerCommands() { + for (const l of subLifecycles) l.registerCommands?.(); + }, + + registerEvents() { + for (const l of subLifecycles) l.registerEvents?.(); + }, + + async init() { + // 先加载统一区域配置,子生命周期再各自 init(读缓存) + await initAreaConfig(); + for (const l of subLifecycles) await l.init?.(); + }, + + cleanup() { + for (const l of subLifecycles) l.cleanup?.(); + }, + }, +}); + +/* ── 重导出:同进程其它模块 / 平台服务派发用 ─────────────── */ +export { areaByName, areaByPoint, allAreas, areasByFeature } from "./area-service.js"; +export type { Area, AreaConfig, AreaFeatures, FeatureName } from "./area-types.js"; diff --git a/modules/packages/feature-area/sapi/src/lifecycles/clean.ts b/modules/packages/feature-area/sapi/src/lifecycles/clean.ts new file mode 100644 index 0000000..df8aca9 --- /dev/null +++ b/modules/packages/feature-area/sapi/src/lifecycles/clean.ts @@ -0,0 +1,214 @@ +/** + * lifecycles/clean.ts — 掉落物清理(合并自 v1 feature-clean) + * + * 逻辑保持不变:定时扫描全服掉落物,超过阈值则集中收纳到固定回收箱阵列, + * kill_list 中的物品直接清除;回收箱装满则顺延到下一个,并放置记录时间的告示牌。 + * + * 与其它子功能的差异(重要): + * clean 不是"区域范围"语义 —— 它扫描全服掉落物、收纳到唯一的回收箱阵列。 + * 为纳入统一区域模块,约定:只要任意区域的 features.clean === true 即视为 + * clean 子功能"已启用"(isFeatureActive("clean")),否则定时任务空转。 + * 回收箱几何 / kill_list / 阈值等改从 configs/area.json 的 clean 配置块读取 + * (SDK config.get),不再用 ConfigManager.getClean()/getGrid()/getSetting()。 + */ + +import { BlockComponentTypes, Entity, ItemStack, system, world } from "@minecraft/server"; +import { Command, debug, Permission } from "@sfmc/sdk/sapi/runtime"; +import { + ensureDoubleChest, + getBase, + getChestCardinal, + getShanghaiTime, + getSignFacing, + placeSign, +} from "@sfmc/sdk/sapi/runtime"; +import type { ModuleLifecycle } from "@sfmc/sdk/module-loader"; +import { getCleanConfig, isFeatureActive } from "../area-service.js"; + +let _cleanIndex = 0; + +let _startPoint: [number, number, number] = [0, 0, 0]; +let _size: [number, number] = [5, 5]; +let _direction = -1; // 箱子的朝向 +let _face = -1; +let _killList: string[] = []; +let _itemMax = 128; +let _timeout = 60; +let _intervalId: number | undefined = undefined; + +/** + * 将物品放入回收箱阵列。 + * @param itemProvider 物品给予函数,返回 undefined 表示任务结束 + * @param isFirstCall 首次调用;若一轮循环后仍有物品,归零索引再循环一次 + */ +function placeItem(itemProvider: () => ItemStack | undefined, isFirstCall = true): void { + const base = getBase(_direction); + const cardinalDirection = getChestCardinal(_direction, _face); + const facingDirection = getSignFacing(_direction, _face); + + let index = 0; + const currentIndex = _cleanIndex; // 当前箱子索引,仅在跳过阶段使用 + const dimension = world.getDimension("overworld"); + for (let mainAxis = 0; mainAxis < _size[0]; mainAxis++) { + for (let y = 0; y < _size[1]; y++) { + // 若还未到达当前空箱子的索引,则跳过 + index++; + if (index < currentIndex) { + continue; + } + const coordinate = { + x: _startPoint[0] + mainAxis * base[0] * 2, + y: _startPoint[1] + y, + z: _startPoint[2] + mainAxis * base[1] * 2, + }; // 主箱子 + const coordinate2 = { + x: coordinate.x + base[0], + y: coordinate.y, + z: coordinate.z + base[1], + }; // 主箱子右边一个箱子 + const block = dimension.getBlock(coordinate); + // 方块应该是箱子,不是则放置 + ensureDoubleChest(dimension, coordinate, cardinalDirection, _direction); + + const inventory = block!.getComponent(BlockComponentTypes.Inventory) as + | { container?: { emptySlotsCount: number; clearAll(): void; addItem(item: ItemStack): void } } + | undefined; + if (!inventory || !inventory.container) { + continue; + } + const container = inventory.container; + if (container.emptySlotsCount === 0) { + container.clearAll(); + } + + while (container.emptySlotsCount > 0) { + const item = itemProvider(); + if (!item) { + // 没有物品需要处理了,结束任务 + return; + } + container.addItem(item); + } + // 当前箱子装满了,更新索引为下一个 + _cleanIndex = index + 1; + // 放置告示牌 + const signCoordinate = { + x: coordinate2.x + (base[0] !== 0 ? 0 : _face), + y: coordinate2.y, + z: coordinate2.z + (base[1] !== 0 ? 0 : _face), + }; + placeSign(dimension, signCoordinate, facingDirection, getTimeStr()); + } + } + // 一轮循环后仍未结束,归零,进行新一轮 + _cleanIndex = 0; + if (isFirstCall) { + placeItem(itemProvider, false); + } +} + +/** 开始清理 */ +function startClean(entities?: Entity[]): void { + debug.i("CLEAN", `startClean: entityCount=${entities?.length || "all"}`); + const itemEntities = entities ?? getAllItemEntities(); + + placeItem(() => { + while (itemEntities.length > 0) { + const itemEntity = itemEntities.pop()!; + const stack = (itemEntity.getComponent("minecraft:item") as { itemStack?: ItemStack } | undefined)?.itemStack; + if (!stack) { + continue; + } + // 在直接清除列表中的物品实体直接清除 + if (_killList.some((value) => value === stack.typeId)) { + itemEntity.kill(); + continue; + } + itemEntity.kill(); + return stack; + } + return undefined; + }); +} + +function startCleanInterval(): void { + debug.i("CLEAN", "startCleanInterval"); + if (_intervalId) { + system.clearRun(_intervalId); + _intervalId = undefined; + } + _intervalId = system.runInterval(() => { + if (!isFeatureActive("clean")) return; + const entities = getAllItemEntities(); + if (entities.length > _itemMax) { + world.sendMessage({ rawtext: [{ text: "「§6読経するヤマビコ ~ 幽谷 響子§f」 距离清理掉落物还有§c 5 §fs" }] }); + system.runTimeout(() => { + startClean(undefined); + system.runTimeout(() => { + world.sendMessage({ rawtext: [{ text: "§a* 已清理掉落物 *" }] }); + }, 5); + }, 100); + } + }, _timeout * 20); +} + +function stopCleanInterval(): void { + debug.i("CLEAN", "stopCleanInterval"); + if (_intervalId) { + system.clearRun(_intervalId); + _intervalId = undefined; + } +} + +/** 获取世界的所有掉落物实体 */ +function getAllItemEntities(): Entity[] { + const itemEntities = world.getDimension("overworld").getEntities({ type: "item" }); + itemEntities.push(...world.getDimension("nether").getEntities({ type: "item" })); + itemEntities.push(...world.getDimension("the_end").getEntities({ type: "item" })); + return itemEntities; +} + +function getTimeStr(): string { + const { date, time } = getShanghaiTime(); + return `\n${date}\n${time}`; +} + +export const cleanLifecycle: ModuleLifecycle = { + registerPermissions() { + Permission.register("clean.admin", Permission.OP); + }, + + registerCommands() { + Command.register( + "clean", + "clean.admin", + () => { + startClean(undefined); + }, + "开始扫地", + "clean" + ); + }, + + init() { + debug.i("CLEAN", "Clean.init"); + const cfg = getCleanConfig(); + if (cfg) { + if (cfg.recycle_bin) { + _startPoint = [cfg.recycle_bin.start[0], cfg.recycle_bin.start[1], cfg.recycle_bin.start[2]]; + _size = [cfg.recycle_bin.size[0], cfg.recycle_bin.size[1]]; + _direction = cfg.recycle_bin.direction; + _face = cfg.recycle_bin.face; + } + _killList = Array.isArray(cfg.kill_list) ? cfg.kill_list : []; + _itemMax = typeof cfg.item_max === "number" ? cfg.item_max : 128; + _timeout = typeof cfg.poll_interval === "number" ? cfg.poll_interval : 60; + } + startCleanInterval(); + }, + + cleanup() { + debug.i("CLEAN", "Clean.stop"); + stopCleanInterval(); + }, +}; diff --git a/modules/packages/feature-area/sapi/src/lifecycles/creative.ts b/modules/packages/feature-area/sapi/src/lifecycles/creative.ts new file mode 100644 index 0000000..473e8bb --- /dev/null +++ b/modules/packages/feature-area/sapi/src/lifecycles/creative.ts @@ -0,0 +1,317 @@ +/** + * lifecycles/creative.ts — 区域创造(合并自 v1 feature-area-creative) + * + * 逻辑保持不变:进出"开启 creative 的区域"自动切换创造/生存模式,区域内限制 + * 方块放置/破坏,禁放违禁物,区域内刷怪清除,边界快速检测,计分项保存/恢复。 + * 变更点: + * - 区域判定改走 area-service(SDK config),不再用 ConfigManager.getAreas("creative") + * - 违禁物列表改从 configs/area.json 的 creative.banned_items 读取(SDK config.get), + * 不再用 ConfigManager.getBannedItems() + * - 连锁开关 _chainEnabled 同时约束 creative + survival(与 v1 CreativeArea.enable 等价), + * 由 survival 子生命周期通过 isCreativeChainEnabled() 读取 + */ + +import { + Entity, + EntityInitializationCause, + EntitySpawnAfterEvent, + GameMode, + Player, + PlayerBreakBlockBeforeEvent, + PlayerDimensionChangeAfterEvent, + PlayerPlaceBlockBeforeEvent, + PlayerSpawnAfterEvent, + system, + world, +} from "@minecraft/server"; +import { Msg, Permission, pointInArea_2D } from "@sfmc/sdk/sapi/runtime"; +import { config } from "@sfmc/sdk/sapi/config"; +import type { ModuleLifecycle } from "@sfmc/sdk/module-loader"; +import { areasByFeature, pointInFeatureArea } from "../area-service.js"; + +interface CreativeConfig { + banned_items?: string[]; +} + +const BORDER_THRESHOLD = 10; +const BUFFER_ZONE = 3; + +/** 连锁开关:同时控制 creative + survival(等价 v1 CreativeArea.enable) */ +let _chainEnabled = true; +let _bannedItems: string[] = []; + +// 事件退订 thunk(SAPI 的 event.subscribe 返回回调本身,退订需 event.unsubscribe(cb)) +const _eventCleanups: Array<() => void> = []; +const _tickRunIds: number[] = []; + +/** 连锁开关状态(供 survival 子生命周期读取) */ +export function isCreativeChainEnabled(): boolean { + return _chainEnabled; +} + +/* ── 区域判定 ─────────────────────────────────────────────── */ + +function inArea(entity: Entity): string | undefined { + const area = pointInFeatureArea("creative", entity.dimension.id, entity.location.x, entity.location.z); + return area ? area.name : undefined; +} + +function inAreaByPos(x: number, z: number, dimensionId: string): boolean { + for (const area of areasByFeature("creative")) { + if (dimensionId === area.dimension) { + if (pointInArea_2D(x, z, area.start[0], area.start[1], area.end[0], area.end[1])) { + return true; + } + } + } + return false; +} + +function isNearBorder(entity: Entity, threshold: number = BORDER_THRESHOLD): boolean { + for (const area of areasByFeature("creative")) { + if (entity.dimension.id !== area.dimension) continue; + const minX = Math.min(area.start[0], area.end[0]) - threshold; + const maxX = Math.max(area.start[0], area.end[0]) + threshold; + const minZ = Math.min(area.start[1], area.end[1]) - threshold; + const maxZ = Math.max(area.start[1], area.end[1]) + threshold; + if ( + entity.location.x >= minX && + entity.location.x <= maxX && + entity.location.z >= minZ && + entity.location.z <= maxZ + ) + return true; + } + return false; +} + +function inBufferZone(entity: Entity): boolean { + for (const area of areasByFeature("creative")) { + if (entity.dimension.id !== area.dimension) continue; + const minX = Math.min(area.start[0], area.end[0]); + const maxX = Math.max(area.start[0], area.end[0]); + const minZ = Math.min(area.start[1], area.end[1]); + const maxZ = Math.max(area.start[1], area.end[1]); + const x = entity.location.x; + const z = entity.location.z; + const inExpanded = + x >= minX - BUFFER_ZONE && x <= maxX + BUFFER_ZONE && z >= minZ - BUFFER_ZONE && z <= maxZ + BUFFER_ZONE; + if (!inExpanded) continue; + if (x >= minX && x <= maxX && z >= minZ && z <= maxZ) continue; + return true; + } + return false; +} + +function creativeDims(): Set { + const dims = new Set(); + for (const area of areasByFeature("creative")) dims.add(area.dimension); + return dims; +} + +/* ── 进入 / 离开 处理(背包由 InventorySwitcher 接管) ─────── */ + +function enterArea(player: Player, areaName: string): void { + saveScores(player); + player.setGameMode(GameMode.Creative); + player.setDynamicProperty("hpbe:creative_area", areaName); + Msg.info(`进入 §a${areaName}创造区域§r ,切换为创造模式。`, player); +} + +function leaveArea(player: Player, areaName: string): void { + restoreScores(player); + player.setGameMode(GameMode.Survival); + player.setDynamicProperty("hpbe:creative_area", undefined); + Msg.info(`离开 §a${areaName}创造区域§r ,恢复生存模式。`, player); +} + +/* ── 计分项保存 / 恢复 ─────────────────────────────────────── */ + +function saveScores(player: Player): void { + const identity = player.scoreboardIdentity; + if (!identity) return; + const scores: Record = {}; + for (const obj of world.scoreboard.getObjectives()) { + try { + const score = obj.getScore(identity); + if (score !== undefined) scores[obj.id] = score; + } catch { + /* ignore */ + } + } + if (Object.keys(scores).length > 0) { + player.setDynamicProperty("hpbe:creative_scores", JSON.stringify(scores)); + } +} + +function restoreScores(player: Player): void { + const raw = player.getDynamicProperty("hpbe:creative_scores") as string | undefined; + const scores: Record | undefined = raw ? JSON.parse(raw) : undefined; + if (!scores) return; + const identity = player.scoreboardIdentity; + if (!identity) return; + for (const obj of world.scoreboard.getObjectives()) { + const v = scores[obj.id]; + if (v !== undefined) { + try { + obj.setScore(identity, v); + } catch { + /* ignore */ + } + } + } + player.setDynamicProperty("hpbe:creative_scores", undefined); +} + +/* ── 定时扫描(进出检测) ───────────────────────────────────── */ + +function startTick(): void { + _tickRunIds.push( + system.runInterval(() => { + if (!_chainEnabled) return; + for (const player of world.getPlayers()) { + if (player.getGameMode() === GameMode.Spectator) continue; + const currentArea = player.getDynamicProperty("hpbe:creative_area") as string | undefined; + if (currentArea === undefined) { + const areaName = inArea(player); + if (areaName !== undefined) enterArea(player, areaName); + } else if (inArea(player) === undefined) { + leaveArea(player, currentArea); + } + } + }, 10) + ); +} + +function startBorderFastCheck(): void { + _tickRunIds.push( + system.runInterval(() => { + if (!_chainEnabled) return; + for (const player of world.getPlayers()) { + if (player.getGameMode() !== GameMode.Creative) continue; + if (!isNearBorder(player)) continue; + const currentArea = player.getDynamicProperty("hpbe:creative_area") as string | undefined; + if (currentArea !== undefined && inArea(player) === undefined) { + leaveArea(player, currentArea); + } + } + }, 2) + ); +} + +export const creativeLifecycle: ModuleLifecycle = { + registerPermissions() { + Permission.register("creativearea.place_banned", Permission.Admin); + }, + + registerEvents() { + if (_eventCleanups.length > 0) return; + + const spawnCb = world.afterEvents.playerSpawn.subscribe((event: PlayerSpawnAfterEvent) => { + if (!event.initialSpawn) return; + system.runTimeout(() => { + const areaName = inArea(event.player); + if (areaName !== undefined) { + enterArea(event.player, areaName); + } else if ( + event.player.getGameMode() === GameMode.Creative || + event.player.getGameMode() === GameMode.Spectator + ) { + event.player.setGameMode(GameMode.Survival); + } + }, 60); + }); + _eventCleanups.push(() => world.afterEvents.playerSpawn.unsubscribe(spawnCb)); + + const dimCb = world.afterEvents.playerDimensionChange.subscribe((event: PlayerDimensionChangeAfterEvent) => { + if (!_chainEnabled) return; + system.runTimeout(() => { + const areaName = inArea(event.player); + const currentArea = event.player.getDynamicProperty("hpbe:creative_area") as string | undefined; + if (currentArea === undefined && areaName !== undefined) { + enterArea(event.player, areaName); + } else if (currentArea !== undefined && areaName === undefined) { + leaveArea(event.player, currentArea); + } + }, 10); + }); + _eventCleanups.push(() => world.afterEvents.playerDimensionChange.unsubscribe(dimCb)); + + const entityCb = world.afterEvents.entitySpawn.subscribe((event: EntitySpawnAfterEvent) => { + if (!_chainEnabled) return; + if (!event.entity) return; + if (event.entity.typeId === "minecraft:player") return; + if (!creativeDims().has(event.entity.dimension.id)) return; + try { + if (event.cause === EntityInitializationCause.Spawned) { + if (inArea(event.entity) !== undefined || inBufferZone(event.entity)) { + event.entity.remove(); + } + } + } catch { + /* ignore */ + } + }); + _eventCleanups.push(() => world.afterEvents.entitySpawn.unsubscribe(entityCb)); + + const placeCb = world.beforeEvents.playerPlaceBlock.subscribe((event: PlayerPlaceBlockBeforeEvent) => { + if (!_chainEnabled) return; + const player = event.player; + if (player.getGameMode() !== GameMode.Creative) return; + if (!inAreaByPos(event.block.location.x, event.block.location.z, player.dimension.id)) { + event.cancel = true; + Msg.error(`你只能在创造区域内放置方块。`, player); + return; + } + if (_bannedItems.indexOf(event.permutationToPlace.type.id) !== -1) { + if (!Permission.check(player, "creativearea.place_banned")) { + event.cancel = true; + Msg.error(`创造区域内禁止放置 ${event.permutationToPlace.type.id}。`, player); + } + } + }); + _eventCleanups.push(() => world.beforeEvents.playerPlaceBlock.unsubscribe(placeCb)); + + const breakCb = world.beforeEvents.playerBreakBlock.subscribe((event: PlayerBreakBlockBeforeEvent) => { + if (!_chainEnabled) return; + if (event.player.getGameMode() !== GameMode.Creative) return; + if (!inAreaByPos(event.block.location.x, event.block.location.z, event.player.dimension.id)) { + event.cancel = true; + Msg.error(`你只能破坏创造区域内的方块。`, event.player); + } + }); + _eventCleanups.push(() => world.beforeEvents.playerBreakBlock.unsubscribe(breakCb)); + }, + + async init() { + const cfg = await config.get("creative"); + _bannedItems = Array.isArray(cfg?.banned_items) ? cfg!.banned_items : []; + config.onChange((key, value) => { + if (key === "creative" && value && typeof value === "object") { + const items = (value as CreativeConfig).banned_items; + _bannedItems = Array.isArray(items) ? items : []; + } + }); + startTick(); + startBorderFastCheck(); + }, + + cleanup() { + for (const off of _eventCleanups) { + try { + off(); + } catch { + /* ignore */ + } + } + _eventCleanups.length = 0; + for (const id of _tickRunIds) { + try { + system.clearRun(id); + } catch { + /* ignore */ + } + } + _tickRunIds.length = 0; + }, +}; diff --git a/modules/packages/feature-area/sapi/src/lifecycles/fly.ts b/modules/packages/feature-area/sapi/src/lifecycles/fly.ts new file mode 100644 index 0000000..8d6e246 --- /dev/null +++ b/modules/packages/feature-area/sapi/src/lifecycles/fly.ts @@ -0,0 +1,125 @@ +/** + * lifecycles/fly.ts — 区域飞行(合并自 v1 feature-area-fly) + * + * 逻辑保持不变:进入"开启 fly 的区域"自动开启飞行,离开则关闭并把玩家落地。 + * 变更点: + * - 区域判定改走 area-service(SDK config),不再用 ConfigManager.getAreas("fly") + * - 扫描周期改读 configs/area.json 的 scan_interval_ticks(默认 40) + */ + +import { Entity, GameMode, Player, PlayerSpawnAfterEvent, system, world } from "@minecraft/server"; +import { debug, Msg, Permission } from "@sfmc/sdk/sapi/runtime"; +import type { ModuleLifecycle } from "@sfmc/sdk/module-loader"; +import { getScanIntervalTicks, pointInFeatureArea } from "../area-service.js"; + +let _scanRunId: number | undefined = undefined; +let _spawnCb: ((event: PlayerSpawnAfterEvent) => void) | undefined = undefined; + +/** 实体是否在开启 fly 的区域内,命中返回区域名 */ +function inFlyArea(entity: Entity): string | undefined { + const area = pointInFeatureArea("fly", entity.dimension.id, entity.location.x, entity.location.z); + return area ? area.name : undefined; +} + +function enableFly(player: Player): void { + try { + player.runCommand("gamerule sendcommandfeedback false"); + player.runCommand("ability @s mayfly true"); + player.runCommand("gamerule sendcommandfeedback true"); + } catch { + console.warn("§c由于新版移除了相关指令,请在世界中开启教育模式。"); + } +} + +function disableFly(player: Player): void { + const res = player.dimension.getBlockFromRay( + player.location, + { x: 0, y: -1, z: 0 }, + { includeLiquidBlocks: true, includePassableBlocks: false } + ); + if (res !== undefined) { + player.teleport({ x: res.block.location.x, y: res.block.location.y + 1, z: res.block.location.z }); + } + try { + player.runCommand("gamerule sendcommandfeedback false"); + player.runCommand("ability @s mayfly false"); + player.runCommand("gamemode adventure"); + player.runCommand("gamemode survival"); + player.runCommand("gamerule sendcommandfeedback true"); + } catch { + /* ignore */ + } +} + +/** 玩家进服时的一次性检测 */ +function playerJoinEvent(player: Player): void { + system.runTimeout(() => { + const areaName = inFlyArea(player); + if (areaName !== undefined) { + enableFly(player); + Msg.info(`当前处于飞行区 ${areaName}, 已打开飞行模式。`, player); + player.setDynamicProperty("hpbe:dogefly", areaName); + } + }, 60); +} + +function startScan(): void { + if (_scanRunId !== undefined) return; + _scanRunId = system.runInterval(() => { + for (const player of world.getPlayers({ gameMode: GameMode.Survival })) { + const nowArea = player.getDynamicProperty("hpbe:dogefly") as string | undefined; + const areaName = inFlyArea(player); + + if (areaName !== undefined) { + if (nowArea === undefined) { + enableFly(player); + Msg.info(`当前处于飞行区 ${areaName}, 已打开飞行模式。`, player); + player.setDynamicProperty("hpbe:dogefly", areaName); + } else if (nowArea !== areaName) { + player.setDynamicProperty("hpbe:dogefly", areaName); + } + } else if (nowArea !== undefined) { + disableFly(player); + Msg.info(`离开飞行区 ${nowArea}, 已关闭飞行模式。`, player); + player.setDynamicProperty("hpbe:dogefly", undefined); + } + } + }, getScanIntervalTicks()); +} + +export const flyLifecycle: ModuleLifecycle = { + registerPermissions() { + Permission.register("fly.use", Permission.Any); + }, + + registerEvents() { + if (_spawnCb) return; + debug.i("FLY", "registerEvents"); + _spawnCb = world.afterEvents.playerSpawn.subscribe((event) => { + if (event.initialSpawn) playerJoinEvent(event.player); + }); + }, + + init() { + startScan(); + }, + + cleanup() { + if (_scanRunId !== undefined) { + try { + system.clearRun(_scanRunId); + } catch { + /* ignore */ + } + _scanRunId = undefined; + } + if (_spawnCb) { + try { + world.afterEvents.playerSpawn.unsubscribe(_spawnCb); + } catch { + /* ignore */ + } + _spawnCb = undefined; + } + }, +}; diff --git a/modules/packages/feature-area/sapi/src/lifecycles/peace.ts b/modules/packages/feature-area/sapi/src/lifecycles/peace.ts new file mode 100644 index 0000000..140d527 --- /dev/null +++ b/modules/packages/feature-area/sapi/src/lifecycles/peace.ts @@ -0,0 +1,90 @@ +/** + * lifecycles/peace.ts — 区域和平(合并自 v1 feature-area-peace) + * + * 逻辑保持不变:实体在"开启 peace 的区域"内且命中怪物过滤器时,生成即移除。 + * 变更点: + * - 区域判定改走 area-service(SDK config),不再用 ConfigManager.getAreas("peace") + * - 怪物过滤器改从 configs/area.json 的 peace.filters 读取(SDK config.get), + * 不再用 ConfigManager.getPeaceFilters() + */ + +import { Entity, EntityInitializationCause, EntitySpawnAfterEvent, world } from "@minecraft/server"; +import { config } from "@sfmc/sdk/sapi/config"; +import type { ModuleLifecycle } from "@sfmc/sdk/module-loader"; +import { isFeatureActive, pointInFeatureArea } from "../area-service.js"; + +interface PeaceFilter { + family?: string; + exclude_family?: string; +} + +interface PeaceConfig { + filters?: PeaceFilter[]; +} + +let _filters: PeaceFilter[] = []; +let _spawnCb: ((event: EntitySpawnAfterEvent) => void) | undefined = undefined; + +/** 实体是否在开启 peace 的区域内 */ +function inPeaceArea(entity: Entity): boolean { + return ( + pointInFeatureArea("peace", entity.dimension.id, entity.location.x, entity.location.z) !== null + ); +} + +/** 依据过滤器组装 EntityQueryOptions(families / excludeFamilies) */ +function getPeaceEntityQO(): { families?: string[]; excludeFamilies?: string[] } { + const qo: { families?: string[]; excludeFamilies?: string[] } = {}; + for (const f of _filters) { + if (f.family) { + if (!qo.families) qo.families = []; + qo.families.push(f.family); + } + if (f.exclude_family) { + if (!qo.excludeFamilies) qo.excludeFamilies = []; + qo.excludeFamilies.push(f.exclude_family); + } + } + return qo; +} + +export const peaceLifecycle: ModuleLifecycle = { + registerEvents() { + if (_spawnCb) return; + _spawnCb = world.afterEvents.entitySpawn.subscribe((event) => { + if (!isFeatureActive("peace")) return; + try { + if (event.cause === EntityInitializationCause.Spawned) { + const entity = event.entity; + if (inPeaceArea(entity) && entity.matches(getPeaceEntityQO())) { + entity.remove(); + } + } + } catch { + /* 实体已失效等,忽略 */ + } + }); + }, + + async init() { + const cfg = await config.get("peace"); + _filters = Array.isArray(cfg?.filters) ? cfg!.filters : []; + config.onChange((key, value) => { + if (key === "peace" && value && typeof value === "object") { + const filters = (value as PeaceConfig).filters; + _filters = Array.isArray(filters) ? filters : []; + } + }); + }, + + cleanup() { + if (_spawnCb) { + try { + world.afterEvents.entitySpawn.unsubscribe(_spawnCb); + } catch { + /* ignore */ + } + _spawnCb = undefined; + } + }, +}; diff --git a/modules/packages/feature-area/sapi/src/lifecycles/survival.ts b/modules/packages/feature-area/sapi/src/lifecycles/survival.ts new file mode 100644 index 0000000..2c54977 --- /dev/null +++ b/modules/packages/feature-area/sapi/src/lifecycles/survival.ts @@ -0,0 +1,114 @@ +/** + * lifecycles/survival.ts — 区域生存(合并自 v1 feature-area-survival) + * + * 逻辑保持不变:创造区以外强制生存,阻止在创造区外手动切到创造/旁观。 + * 同模块耦合说明:本子功能依赖 creative 子功能的"连锁开关"与创造区判定 + * - 连锁开关:isCreativeChainEnabled()(等价 v1 CreativeArea.enable) + * - 创造区判定:area-service.pointInFeatureArea("creative", ...) + * 变更点:区域判定改走 area-service(SDK config),不再用 ConfigManager.getAreas("creative") + */ + +import { + Entity, + GameMode, + Player, + PlayerDimensionChangeAfterEvent, + PlayerGameModeChangeBeforeEvent, + PlayerSpawnAfterEvent, + system, + world, +} from "@minecraft/server"; +import { Msg, Permission } from "@sfmc/sdk/sapi/runtime"; +import type { ModuleLifecycle } from "@sfmc/sdk/module-loader"; +import { pointInFeatureArea } from "../area-service.js"; +import { isCreativeChainEnabled } from "./creative.js"; + +let _enabled = true; +// 事件退订 thunk(SAPI 的 event.subscribe 返回回调本身,退订需 event.unsubscribe(cb)) +const _eventCleanups: Array<() => void> = []; + +/** 实体是否在开启 creative 的区域内 */ +function inCreativeArea(entity: Entity): boolean { + return ( + pointInFeatureArea("creative", entity.dimension.id, entity.location.x, entity.location.z) !== null + ); +} + +function forceSurvival(player: Player): void { + player.setGameMode(GameMode.Survival); + Msg.info(`已离开创造区域,强制切换为生存模式。`, player); +} + +export const survivalLifecycle: ModuleLifecycle = { + registerPermissions() { + Permission.register("survivalarea.gamemode.bypass", Permission.OP); + }, + + registerEvents() { + if (_eventCleanups.length > 0) return; + + // 进服时检测 + const spawnCb = world.afterEvents.playerSpawn.subscribe((event: PlayerSpawnAfterEvent) => { + if (!event.initialSpawn) return; + if (!isCreativeChainEnabled()) return; + if (!_enabled) return; + + const player = event.player; + const mode = player.getGameMode(); + if (mode === GameMode.Survival || mode === GameMode.Adventure) return; + + system.runTimeout(() => { + if (!inCreativeArea(player)) { + forceSurvival(player); + } + }, 60); + }); + _eventCleanups.push(() => world.afterEvents.playerSpawn.unsubscribe(spawnCb)); + + // 阻止手动切换到创造/旁观(区外) + const gmCb = world.beforeEvents.playerGameModeChange.subscribe((event: PlayerGameModeChangeBeforeEvent) => { + if (!isCreativeChainEnabled()) return; + if (!_enabled) return; + if (event.toGameMode === GameMode.Creative || event.toGameMode === GameMode.Spectator) { + if (Permission.check(event.player, "survivalarea.gamemode.bypass")) return; + if (!inCreativeArea(event.player)) { + event.cancel = true; + Msg.error(`你当前不在创造区域内,无法切换到该模式。`, event.player); + } + } + }); + _eventCleanups.push(() => world.beforeEvents.playerGameModeChange.unsubscribe(gmCb)); + + // 跨维度传送后检测 + const dimCb = world.afterEvents.playerDimensionChange.subscribe((event: PlayerDimensionChangeAfterEvent) => { + if (!isCreativeChainEnabled()) return; + if (!_enabled) return; + const player = event.player; + const mode = player.getGameMode(); + if (mode === GameMode.Survival || mode === GameMode.Adventure) return; + + system.runTimeout(() => { + if (!inCreativeArea(player)) { + forceSurvival(player); + } + }, 10); + }); + _eventCleanups.push(() => world.afterEvents.playerDimensionChange.unsubscribe(dimCb)); + }, + + init() { + // 核心逻辑已在 registerEvents 中订阅事件 + void _enabled; + }, + + cleanup() { + for (const off of _eventCleanups) { + try { + off(); + } catch { + /* ignore */ + } + } + _eventCleanups.length = 0; + }, +}; diff --git a/modules/packages/clean/sapi/tsconfig.json b/modules/packages/feature-area/sapi/tsconfig.json similarity index 100% rename from modules/packages/clean/sapi/tsconfig.json rename to modules/packages/feature-area/sapi/tsconfig.json diff --git a/modules/packages/fly/package.json b/modules/packages/fly/package.json deleted file mode 100644 index d5c5b99..0000000 --- a/modules/packages/fly/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@sfmc/module-fly", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "SAPI module: fly 模块。SAPI 侧入口,sapi/src/index.ts。", - "main": "sapi/src/index.ts", - "scripts": { - "typecheck": "tsc --noEmit -p sapi/tsconfig.json" - }, - "dependencies": { - "@sfmc/sdk": "*" - }, - "peerDependencies": { - "@minecraft/server": "2.10.0-beta.1.26.40-preview.30" - } -} diff --git a/modules/packages/fly/sapi/manifest.json b/modules/packages/fly/sapi/manifest.json deleted file mode 100644 index 9746087..0000000 --- a/modules/packages/fly/sapi/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "handlers": [], - "routes": [], - "migrations": [], - "notes": "fly: no SAPI→db-server HTTP calls; pure game-side logic" -} diff --git a/modules/packages/fly/sapi/src/Fly.ts b/modules/packages/fly/sapi/src/Fly.ts deleted file mode 100644 index d7cea6c..0000000 --- a/modules/packages/fly/sapi/src/Fly.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* ---------------------------------------- *\ - * Name : 区域飞行 * - * Description : 芜湖 * - * Version : 1.0.0 * - * Author : ENIAC_Jushi * -\* ---------------------------------------- */ - -import { Entity, GameMode, Player, system, world } from "@minecraft/server"; -import { debug } from "@sfmc/sdk/sapi/runtime"; -import { ConfigManager } from "@sfmc/sdk/module-loader"; -import { Permission } from "@sfmc/sdk/sapi/runtime"; -import * as Tool from "@sfmc/sdk/sapi/runtime"; - -export function registerPermissions(): void { - Permission.register("fly.use", Permission.Any); -} - -export function registerEvents(): void { - debug.i("FLY", "registerEvents"); - world.afterEvents.playerSpawn.subscribe((event) => { - if (event.initialSpawn) playerJoinEvent(event.player); - }); -} - -export function init(): void {} - -/** - * 玩家加入事件 - */ -export function playerJoinEvent(player: Player): void { - system.runTimeout(() => { - let areaName = inFlyArea(player); - if (areaName !== undefined) { - enableFly(player); - Tool.Msg.info(`当前处于飞行区 ${areaName}, 已打开飞行模式。`, player); - player.setDynamicProperty("hpbe:dogefly", areaName); - } - }, 60); -} - -let scanRunId: number | undefined; - -function startScan() { - if (scanRunId !== undefined) return; - scanRunId = system.runInterval(() => { - for (let player of world.getPlayers({ gameMode: GameMode.Survival })) { - let nowArea = player.getDynamicProperty("hpbe:dogefly") as string | undefined; - let areaName = inFlyArea(player); - - if (areaName !== undefined) { - if (nowArea === undefined) { - enableFly(player); - Tool.Msg.info(`当前处于飞行区 ${areaName}, 已打开飞行模式。`, player); - player.setDynamicProperty("hpbe:dogefly", areaName); - } else if (nowArea !== areaName) { - player.setDynamicProperty("hpbe:dogefly", areaName); - } - } else { - if (nowArea !== undefined) { - disableFly(player); - Tool.Msg.info(`离开飞行区 ${nowArea}, 已关闭飞行模式。`, player); - player.setDynamicProperty("hpbe:dogefly", undefined); - } - } - } - }, 40); -} - -export function stop(): void { - if (scanRunId !== undefined) { - try { - system.clearRun(scanRunId); - } catch {} - scanRunId = undefined; - } -} - -export function boot(): void { - if (scanRunId === undefined) startScan(); -} - -/** - * 实体是否在飞行区域内 - */ -function inFlyArea(entity: Entity): string | undefined { - for (let area of ConfigManager.getAreas("fly")) { - if (entity.dimension.id === area.dimension) { - if ( - Tool.pointInArea_2D( - entity.location.x, - entity.location.z, - area.start[0], - area.start[1], - area.end[0], - area.end[1] - ) - ) { - return area.name; - } - } - } - return undefined; -} - -function enableFly(player: Player) { - try { - player.runCommand("gamerule sendcommandfeedback false"); - player.runCommand("ability @s mayfly true"); - player.runCommand("gamerule sendcommandfeedback true"); - } catch (_) { - console.warn("§c由于新版移除了相关指令,请在世界中开启教育模式。"); - } -} - -function disableFly(player: Player) { - let res = player.dimension.getBlockFromRay( - player.location, - { x: 0, y: -1, z: 0 }, - { includeLiquidBlocks: true, includePassableBlocks: false } - ); - if (res !== undefined) { - player.teleport({ x: res.block.location.x, y: res.block.location.y + 1, z: res.block.location.z }); - } - - try { - player.runCommand("gamerule sendcommandfeedback false"); - player.runCommand("ability @s mayfly false"); - player.runCommand("gamemode adventure"); - player.runCommand("gamemode survival"); - player.runCommand("gamerule sendcommandfeedback true"); - } catch (_) {} -} \ No newline at end of file diff --git a/modules/packages/fly/sapi/src/index.ts b/modules/packages/fly/sapi/src/index.ts deleted file mode 100644 index d352215..0000000 --- a/modules/packages/fly/sapi/src/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @sfmc/module-fly — SAPI 侧入口 - * - * 暴露给行为包启动期 (ModuleRegistry.register 调用点): - * - Fly.registerPermissions / registerEvents / boot / stop - * - * entry.ts 通过 `import * as Fly from "@sfmc/module-fly"` 命名空间形式消费, - * 与原 `import * as Fly from "./area/Fly.js"` 完全等价(都靠 ES 模块命名空间 - * 把若干具名导出折叠到一个对象上)。 - */ - -export { registerPermissions, registerEvents, init, boot, stop, playerJoinEvent } from "./Fly.js"; \ No newline at end of file diff --git a/modules/packages/fly/sapi/tsconfig.json b/modules/packages/fly/sapi/tsconfig.json deleted file mode 100644 index 85c1345..0000000 --- a/modules/packages/fly/sapi/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../sdk/@sfmc-sdk/tsconfig.json", - "compilerOptions": { - "noEmit": true, - "rootDir": "../../../.." - }, - "include": ["src/**/*"] -} diff --git a/modules/packages/peace/package.json b/modules/packages/peace/package.json deleted file mode 100644 index 980679f..0000000 --- a/modules/packages/peace/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@sfmc/module-peace", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "SAPI module: peace 模块。SAPI 侧入口,sapi/src/index.ts。", - "main": "sapi/src/index.ts", - "scripts": { - "typecheck": "tsc --noEmit -p sapi/tsconfig.json" - }, - "dependencies": { - "@sfmc/sdk": "*" - }, - "peerDependencies": { - "@minecraft/server": "2.10.0-beta.1.26.40-preview.30" - } -} diff --git a/modules/packages/peace/sapi/manifest.json b/modules/packages/peace/sapi/manifest.json deleted file mode 100644 index baee262..0000000 --- a/modules/packages/peace/sapi/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "handlers": [], - "routes": [], - "migrations": [], - "notes": "peace: no SAPI→db-server HTTP calls; pure game-side logic" -} diff --git a/modules/packages/peace/sapi/src/Peace.ts b/modules/packages/peace/sapi/src/Peace.ts deleted file mode 100644 index 4e2df67..0000000 --- a/modules/packages/peace/sapi/src/Peace.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* ---------------------------------------- *\ - * Name : 区域和平 * - * Description : 区域和平 * - * Version : 1.0.0 * - * Author : ENIAC_Jushi * -\* ---------------------------------------- */ - -import { Entity, EntityInitializationCause, world } from "@minecraft/server"; -import * as Tool from "@sfmc/sdk/sapi/runtime"; -import { ConfigManager } from "@sfmc/sdk/module-loader"; - -export class Peace { - static _instance: Peace; - static getInstance() { - if (!Peace._instance) { - Peace._instance = new Peace(); - } - return Peace._instance; - } - - enable = true; - private entitySpawnSub: any = undefined; - - init() { - this.registerEvents(); - } - - registerEvents() { - if (this.entitySpawnSub) return; - this.entitySpawnSub = world.afterEvents.entitySpawn.subscribe((event) => { - if (!this.enable) return; - try { - if (event.cause === EntityInitializationCause.Spawned) { - let entity = event.entity; - if (this.inPeaceArea(entity) && entity.matches(this.getPeaceEntityQO())) { - event.entity.remove(); - } - } - } catch {} - }); - } - - cleanup() { - if (this.entitySpawnSub?.unsubscribe) { - try { - this.entitySpawnSub.unsubscribe(); - } catch {} - } - this.entitySpawnSub = undefined; - } - - /** - * 实体是否在和平区域内 - */ - inPeaceArea(entity: Entity) { - for (let area of ConfigManager.getAreas("peace")) { - if (entity.dimension.id === area.dimension) { - if ( - Tool.pointInArea_2D( - entity.location.x, - entity.location.z, - area.start[0], - area.start[1], - area.end[0], - area.end[1] - ) - ) { - return true; - } - } - } - return false; - } - - switchPeace() { - return (this.enable = !this.enable); - } - - private getPeaceEntityQO(): any { - const filters = ConfigManager.getPeaceFilters(); - const qo: any = {}; - for (const f of filters) { - if (f.family) { - if (!qo.families) qo.families = []; - qo.families.push(f.family); - } - if (f.exclude_family) { - if (!qo.excludeFamilies) qo.excludeFamilies = []; - qo.excludeFamilies.push(f.exclude_family); - } - } - return qo; - } -} \ No newline at end of file diff --git a/modules/packages/peace/sapi/src/index.ts b/modules/packages/peace/sapi/src/index.ts deleted file mode 100644 index 4246298..0000000 --- a/modules/packages/peace/sapi/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @sfmc/module-peace — SAPI 侧入口 - * - * 暴露给行为包启动期 (ModuleRegistry.register 调用点): - * - Peace.getInstance():注册事件、生命周期 - * - * 配套实体 JSON(peace_skeleton/spider/zombie.json)已随本包拷到 - * resource_pack/entities/,stage I 把它们接进 esbuild 打包管线。 - */ - -export { Peace } from "./Peace.js"; \ No newline at end of file diff --git a/modules/packages/peace/sapi/tsconfig.json b/modules/packages/peace/sapi/tsconfig.json deleted file mode 100644 index 85c1345..0000000 --- a/modules/packages/peace/sapi/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../sdk/@sfmc-sdk/tsconfig.json", - "compilerOptions": { - "noEmit": true, - "rootDir": "../../../.." - }, - "include": ["src/**/*"] -} diff --git a/modules/packages/survival/package.json b/modules/packages/survival/package.json deleted file mode 100644 index 894e286..0000000 --- a/modules/packages/survival/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@sfmc/module-survival", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "SAPI module: survival 模块。SAPI 侧入口,sapi/src/index.ts。", - "main": "sapi/src/index.ts", - "scripts": { - "typecheck": "tsc --noEmit -p sapi/tsconfig.json" - }, - "dependencies": { - "@sfmc/sdk": "*" - }, - "peerDependencies": { - "@minecraft/server": "2.10.0-beta.1.26.40-preview.30" - } -} diff --git a/modules/packages/survival/sapi/manifest.json b/modules/packages/survival/sapi/manifest.json deleted file mode 100644 index 6d82fe4..0000000 --- a/modules/packages/survival/sapi/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "handlers": [], - "routes": [], - "migrations": [], - "notes": "survival: no SAPI→db-server HTTP calls; pure game-side logic" -} diff --git a/modules/packages/survival/sapi/src/SurvivalArea.ts b/modules/packages/survival/sapi/src/SurvivalArea.ts deleted file mode 100644 index dbc7645..0000000 --- a/modules/packages/survival/sapi/src/SurvivalArea.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* ---------------------------------------- *\ - * Name : 生存区域 * - * Description : 全图除创造区外强制生存(需配合创造区) * - * Version : 1.0.0 * - * Author : Shiroha7z * -\* ---------------------------------------- */ - -import { - Entity, - GameMode, - PlayerDimensionChangeAfterEvent, - PlayerGameModeChangeBeforeEvent, - PlayerSpawnAfterEvent, - system, - world, -} from "@minecraft/server"; -import { Permission } from "@sfmc/sdk/sapi/runtime"; -import { ConfigManager } from "@sfmc/sdk/module-loader"; -import * as Tool from "@sfmc/sdk/sapi/runtime"; -import { Msg } from "@sfmc/sdk/sapi/runtime"; -import { CreativeArea } from "@sfmc/module-creative"; - -export class SurvivalArea { - static _instance: SurvivalArea; - /** - * @returns {SurvivalArea} - */ - static getInstance(): SurvivalArea { - if (!SurvivalArea._instance) { - SurvivalArea._instance = new SurvivalArea(); - } - return SurvivalArea._instance; - } - - enable = true; - - private subscriptions: Array = []; - - /** 注册命令和权限(由 entry.ts 在 startup 阶段调用) */ - registerCommandsAndPermissions() { - Permission.register("survivalarea.gamemode.bypass", Permission.OP); - } - - /** 注册事件(由 entry.ts 统一调用) */ - registerEvents() { - if (this.subscriptions.length > 0) return; - // 进服时检测 - this.subscriptions.push( - world.afterEvents.playerSpawn.subscribe((event: PlayerSpawnAfterEvent) => { - if (!event.initialSpawn) return; - if (!CreativeArea.enable) return; - if (!this.enable) return; - - const player = event.player; - const mode = player.getGameMode(); - if (mode === GameMode.Survival || mode === GameMode.Adventure) return; - - system.runTimeout(() => { - if (!this.inCreativeArea(player)) { - this.forceSurvival(player); - } - }, 60); - }) - ); - - // 阻止手动切换到创造/旁观(区外) - this.subscriptions.push( - world.beforeEvents.playerGameModeChange.subscribe((event: PlayerGameModeChangeBeforeEvent) => { - if (!CreativeArea.enable) return; - if (!this.enable) return; - if (event.toGameMode === GameMode.Creative || event.toGameMode === GameMode.Spectator) { - if (Permission.check(event.player, "survivalarea.gamemode.bypass")) return; - if (!this.inCreativeArea(event.player)) { - event.cancel = true; - Msg.error(`你当前不在创造区域内,无法切换到该模式。`, event.player); - } - } - }) - ); - - // 跨维度传送后检测 - this.subscriptions.push( - world.afterEvents.playerDimensionChange.subscribe((event: PlayerDimensionChangeAfterEvent) => { - if (!CreativeArea.enable) return; - if (!this.enable) return; - const player = event.player; - const mode = player.getGameMode(); - if (mode === GameMode.Survival || mode === GameMode.Adventure) return; - - system.runTimeout(() => { - if (!this.inCreativeArea(player)) { - this.forceSurvival(player); - } - }, 10); - }) - ); - } - - init() { - // 核心逻辑已在 registerEvents 中订阅事件 - } - - cleanup() { - for (const s of this.subscriptions) { - try { - s.unsubscribe(); - } catch {} - } - this.subscriptions = []; - } - - private inCreativeArea(entity: Entity): boolean { - for (const area of ConfigManager.getAreas("creative")) { - if (entity.dimension.id === area.dimension) { - if ( - Tool.pointInArea_2D( - entity.location.x, - entity.location.z, - area.start[0], - area.start[1], - area.end[0], - area.end[1] - ) - ) { - return true; - } - } - } - return false; - } - - private forceSurvival(player: any) { - player.setGameMode(GameMode.Survival); - Msg.info(`已离开创造区域,强制切换为生存模式。`, player); - } -} \ No newline at end of file diff --git a/modules/packages/survival/sapi/src/index.ts b/modules/packages/survival/sapi/src/index.ts deleted file mode 100644 index c675ac6..0000000 --- a/modules/packages/survival/sapi/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @sfmc/module-survival — SAPI 侧入口 - * - * 暴露给行为包启动期 (ModuleRegistry.register 调用点): - * - SurvivalArea.getInstance():注册命令/权限、订阅事件、生命周期 - * - * 跨包依赖通过 @sfmc/module-creative 拉取 CreativeArea.enable 状态。 - */ - -export { SurvivalArea } from "./SurvivalArea.js"; \ No newline at end of file diff --git a/modules/packages/survival/sapi/tsconfig.json b/modules/packages/survival/sapi/tsconfig.json deleted file mode 100644 index 85c1345..0000000 --- a/modules/packages/survival/sapi/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../sdk/@sfmc-sdk/tsconfig.json", - "compilerOptions": { - "noEmit": true, - "rootDir": "../../../.." - }, - "include": ["src/**/*"] -}