Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions bds-tools/pack-manager-lifecycle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,37 @@ describe("pack-manager CLI extensions", () => {
assert.equal(await readLevelName(bds), "My World");
});

it("BDS 路径 helpers 单一权威(Demeter/DRY)", async () => {
const {
bdsWorldsDir,
bdsWorldLevelDir,
worldPackListFile,
configPermissionPath,
hasConfigPermission,
serverPropertiesPath,
ensureConfigPermission,
} = await import("./dist/pack-manager.js");
const bds = path.join(tmp, "bds-paths");
fs.mkdirSync(bds, { recursive: true });
assert.equal(bdsWorldsDir(bds), path.join(bds, "worlds"));
assert.equal(bdsWorldLevelDir(bds, "L1"), path.join(bds, "worlds", "L1"));
assert.equal(
worldPackListFile(bdsWorldsDir(bds), "L1", "behavior"),
path.join(bds, "worlds", "L1", "world_behavior_packs.json")
);
assert.equal(
worldPackListFile(bdsWorldsDir(bds), "L1", "resource"),
path.join(bds, "worlds", "L1", "world_resource_packs.json")
);
assert.equal(serverPropertiesPath(bds), path.join(bds, "server.properties"));
const uuid = "00000000-0000-4000-8000-000000000099";
assert.equal(configPermissionPath(bds, uuid), path.join(bds, "config", uuid, "permission.json"));
assert.equal(hasConfigPermission(bds, uuid), false);
assert.equal(await ensureConfigPermission(bds, uuid), true);
assert.equal(hasConfigPermission(bds, uuid), true);
assert.equal(await ensureConfigPermission(bds, uuid), false);
});

it("readWorldPackListResult 区分缺失与 JSON 损坏(doctor parseFail)", async () => {
const { readWorldPackListResult } = await import("./dist/pack-manager.js");
const worldsDir = path.join(tmp, "worlds-parse");
Expand Down
10 changes: 5 additions & 5 deletions bds-tools/src/cli-pack-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ async function main(): Promise<void> {
...(rpName ? { rpName } : {}),
...(clearRp ? { clearResourcePack: true } : {}),
});
process.stdout.write(`[pack-manager] deployed to ${path.join(bdsRoot, "worlds", level)}\n`);
process.stdout.write(`[pack-manager] deployed to ${mod.bdsWorldLevelDir(path.resolve(bdsRoot), level)}\n`);
return;
}
case "enable-pack": {
Expand Down Expand Up @@ -168,11 +168,11 @@ async function main(): Promise<void> {
case "ensure-permission": {
const bdsRoot = need(args, "bds-root");
const packId = need(args, "pack-id");
const wrote = await mod.ensureConfigPermission(path.resolve(bdsRoot), packId);
const root = path.resolve(bdsRoot);
const wrote = await mod.ensureConfigPermission(root, packId);
const rel = path.relative(root, mod.configPermissionPath(root, packId));
process.stdout.write(
wrote
? `[pack-manager] wrote config/${packId}/permission.json\n`
: `[pack-manager] config/${packId}/permission.json already exists — skipped\n`
wrote ? `[pack-manager] wrote ${rel}\n` : `[pack-manager] ${rel} already exists — skipped\n`
);
return;
}
Expand Down
68 changes: 47 additions & 21 deletions bds-tools/src/pack-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,41 @@ export const SFMC_PERMISSIONS = [
"@minecraft/diagnostics",
] as const;

/** `<bdsRoot>/server.properties` — level-name / telemetry 等共用(DRY) */
export function serverPropertiesPath(bdsRoot: string): string {
return path.join(bdsRoot, "server.properties");
}

/** `<bdsRoot>/worlds` — enable-list / 部署调用方勿再硬拼(Demeter) */
export function bdsWorldsDir(bdsRoot: string): string {
return path.join(bdsRoot, "worlds");
}

/** `<bdsRoot>/worlds/<level>` */
export function bdsWorldLevelDir(bdsRoot: string, levelName: string): string {
return path.join(bdsWorldsDir(bdsRoot), levelName);
}

/** 世界 enable-list JSON 绝对路径(单一权威,供读写两侧) */
export function worldPackListFile(
worldsDir: string,
levelName: string,
kind: "behavior" | "resource"
): string {
const name = kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json";
return path.join(worldsDir, levelName, name);
}

/** `<bdsRoot>/config/<bpUuid>/permission.json` — Script API 权限文件权威路径 */
export function configPermissionPath(bdsRoot: string, bpUuid: string): string {
return path.join(bdsRoot, "config", bpUuid, "permission.json");
}

/** 是否已有 Script API permission.json(只读,供 preflight/status) */
export function hasConfigPermission(bdsRoot: string, bpUuid: string): boolean {
return fs.existsSync(configPermissionPath(bdsRoot, bpUuid));
}

/** 随机生成 BP/RP manifest.json header.uuid (RFC 4122 v4) */
export function randomUuid(): string {
/* crypto.randomUUID 是 Node 19+ 内置,SEA 走 Node 22+,这里直接用 */
Expand Down Expand Up @@ -193,16 +228,14 @@ function parseLevelNameFromProperties(text: string): string {

/** 同步读 level-name(供 sfmc resolveBdsContext 等同进程调用方,DRY)。 */
export function readLevelNameSync(bdsRoot: string): string {
const file = path.join(bdsRoot, "server.properties");
const file = serverPropertiesPath(bdsRoot);
if (!fs.existsSync(file)) return "Bedrock level";
return parseLevelNameFromProperties(fs.readFileSync(file, "utf8"));
}

/** 与 sync 同契约;CLI / async 调用方走此入口(LSP) */
export async function readLevelName(bdsRoot: string): Promise<string> {
const file = path.join(bdsRoot, "server.properties");
if (!fs.existsSync(file)) return "Bedrock level";
const text = await fs.promises.readFile(file, "utf8");
return parseLevelNameFromProperties(text);
return readLevelNameSync(bdsRoot);
}

/**
Expand All @@ -213,10 +246,11 @@ export async function readLevelName(bdsRoot: string): Promise<string> {
* must invoke after restart, because BDS only reads that file at startup.
*/
export async function deployToBDS(opts: DeployOpts): Promise<DeployResult> {
const worldsDir = path.join(opts.bdsRoot, "worlds", opts.levelName);
const bpDst = path.join(worldsDir, "behavior_packs", opts.bpName);
const rpDst = path.join(worldsDir, "resource_packs", opts.rpName ?? `${opts.bpName}-rp`);
await fs.promises.mkdir(worldsDir, { recursive: true });
/* levelDir = worlds/<level>;与 EnablePackOpts.worldsDir(=worlds/) 语义不同 — 勿混用(LSP) */
const levelDir = bdsWorldLevelDir(opts.bdsRoot, opts.levelName);
const bpDst = path.join(levelDir, "behavior_packs", opts.bpName);
const rpDst = path.join(levelDir, "resource_packs", opts.rpName ?? `${opts.bpName}-rp`);
await fs.promises.mkdir(levelDir, { recursive: true });
await fs.promises.rm(bpDst, { recursive: true, force: true });
await copyDirAsync(opts.behaviorPackSrc, bpDst);
await writePermissionsJson(bpDst);
Expand All @@ -238,9 +272,9 @@ export async function deployToBDS(opts: DeployOpts): Promise<DeployResult> {
* @returns true = 新写入; false = 已存在跳过
*/
export async function ensureConfigPermission(bdsRoot: string, bpUuid: string): Promise<boolean> {
const dir = path.join(bdsRoot, "config", bpUuid);
const file = path.join(dir, "permission.json");
const file = configPermissionPath(bdsRoot, bpUuid);
if (fs.existsSync(file)) return false;
const dir = path.dirname(file);
await fs.promises.mkdir(dir, { recursive: true });
const payload = { allowed_modules: [...SFMC_PERMISSIONS] };
const tmp = path.join(dir, `.permission.${process.pid}.tmp`);
Expand Down Expand Up @@ -306,11 +340,7 @@ async function editWorldPackList(
opts: EnablePackOpts,
mode: "enable" | "disable"
): Promise<void> {
const file = path.join(
opts.worldsDir,
opts.levelName,
opts.kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"
);
const file = worldPackListFile(opts.worldsDir, opts.levelName, opts.kind);
/* 与 readWorldPackList 同源解析,再写回(DRY);异步写路径保留原语义 */
let entries: WorldPackEntry[] = readWorldPackList(opts.worldsDir, opts.levelName, opts.kind);
const idx = entries.findIndex((e) => e.pack_id === opts.packUuid);
Expand Down Expand Up @@ -459,11 +489,7 @@ export function readWorldPackListResult(
levelName: string,
kind: "behavior" | "resource"
): WorldPackListReadResult {
const file = path.join(
worldsDir,
levelName,
kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"
);
const file = worldPackListFile(worldsDir, levelName, kind);
if (!fs.existsSync(file)) return { entries: [] };
try {
const arr = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
Expand Down
4 changes: 2 additions & 2 deletions bds-tools/src/server-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* server.properties 辅助:安装/启动时确保 emit-server-telemetry=true
*/
import fs from "node:fs";
import path from "node:path";
import { serverPropertiesPath } from "./pack-manager.js";

export const EMIT_SERVER_TELEMETRY_KEY = "emit-server-telemetry";
export const EMIT_SERVER_TELEMETRY_LINE = "emit-server-telemetry=true";
Expand All @@ -20,7 +20,7 @@ export function ensureEmitServerTelemetry(
bdsRoot: string,
logger?: ServerPropertiesLogger
): boolean {
const file = path.join(bdsRoot, "server.properties");
const file = serverPropertiesPath(bdsRoot);
if (!fs.existsSync(file)) return false;

let text = fs.readFileSync(file, "utf8");
Expand Down
16 changes: 8 additions & 8 deletions bds-tools/src/world-packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import os from "node:os";
import path from "node:path";
import { copyDirAsync } from "./fsx.js";
import {
bdsWorldLevelDir,
bdsWorldsDir,
disablePackInWorld,
enablePackInWorld,
readPackManifestHeader,
Expand Down Expand Up @@ -144,8 +146,8 @@ function listPackDirsIn(parent: string): string[] {

/** 扫描世界内已安装 BP/RP 目录 */
export function listInstalledWorldPacks(bdsRoot: string, levelName: string): InstalledWorldPack[] {
const worldRoot = path.join(bdsRoot, "worlds", levelName);
const worldsDir = path.join(bdsRoot, "worlds");
const worldRoot = bdsWorldLevelDir(bdsRoot, levelName);
const worldsDir = bdsWorldsDir(bdsRoot);
const result: InstalledWorldPack[] = [];

for (const kind of ["behavior", "resource"] as const) {
Expand Down Expand Up @@ -351,7 +353,7 @@ export async function enableInstalledPack(opts: {
info: PackManifestInfo;
}): Promise<void> {
await enablePackInWorld({
worldsDir: path.join(opts.bdsRoot, "worlds"),
worldsDir: bdsWorldsDir(opts.bdsRoot),
levelName: opts.levelName,
kind: opts.info.kind,
packUuid: opts.info.uuid,
Expand All @@ -368,7 +370,7 @@ export async function disableInstalledPack(opts: {
version: [number, number, number];
}): Promise<void> {
await disablePackInWorld({
worldsDir: path.join(opts.bdsRoot, "worlds"),
worldsDir: bdsWorldsDir(opts.bdsRoot),
levelName: opts.levelName,
kind: opts.kind,
packUuid: opts.packUuid,
Expand All @@ -382,9 +384,7 @@ export function worldPackParentDir(
kind: WorldPackKind
): string {
return path.join(
bdsRoot,
"worlds",
levelName,
bdsWorldLevelDir(bdsRoot, levelName),
kind === "behavior" ? "behavior_packs" : "resource_packs"
);
}
Expand All @@ -404,7 +404,7 @@ export function listWorldEnableListResult(
levelName: string,
kind: WorldPackKind
): WorldPackListReadResult {
return readWorldPackListResult(path.join(bdsRoot, "worlds"), levelName, kind);
return readWorldPackListResult(bdsWorldsDir(bdsRoot), levelName, kind);
}

export function findInstalledPackById(
Expand Down
Loading