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
21 changes: 18 additions & 3 deletions src/Command/commandClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class Command {

/**添加一条分支并在其中添加一条参数 */
addParam(param: ParamDefinition) {
this.paramBranches.push(param);
this.paramBranches.push(Command.cloneParam(param));
return this;
}
/**添加一条参数分支的多个参数 */
Expand All @@ -94,9 +94,8 @@ export class Command {
.map((param) => {
if (Array.isArray(param)) {
return Command.toTreeParam(param);
} else {
return param;
}
return Command.cloneParam(param);
})
.filter((p) => p != undefined)
.sort((a, b) => {
Expand Down Expand Up @@ -136,6 +135,7 @@ export class Command {
if (param) subParams.push(param);
}
} else {
branch = Command.cloneParam(branch);
if (branch.branches) {
const params = this.fromParamBranches(branch.branches);
branch.subParams = [...(branch.subParams ?? []), ...params];
Expand All @@ -149,6 +149,7 @@ export class Command {
});
}
private static toTreeParam(params: ParamDefinition[]): ParamDefinition | undefined {
params = params.map((param) => Command.cloneParam(param));
for (let i = 0; i < params.length; i++) {
const param = params[i];
if (param.branches && param.branches.length != 0) {
Expand All @@ -164,6 +165,20 @@ export class Command {
return params[0];
}

/** Creates an independent parameter tree so caller-owned command definitions remain reusable. */
private static cloneParam(param: ParamDefinition): ParamDefinition {
return {
...param,
enums: param.enums ? [...param.enums] : undefined,
branches: param.branches?.map((branch) =>
Array.isArray(branch)
? branch.map((child) => Command.cloneParam(child))
: Command.cloneParam(branch)
),
subParams: param.subParams?.map((child) => Command.cloneParam(child)),
};
}

/**转换为原生命令以便注册(内部调用) */
toNative(nameSpace: string) {
const branch = this.getFlatBranch(nameSpace);
Expand Down
17 changes: 11 additions & 6 deletions src/Command/parser/ParamTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,14 @@ export const paramParser: Record<keyof typeof paramTypes, paramParserDefinition>
},
string: {
parser(value) {
return new ParseInfo(value[0]);
const text = value[0];
return new ParseInfo(
text.length >= 2 && text.startsWith('"') && text.endsWith('"')
? text.slice(1, -1)
: text
);
},
regex: new RegExp(/^[^\x20]+$/),
regex: new RegExp(/^(?:"[^"]*"|[^\x20]+)$/),
},
position: {
parser(value, context) {
Expand Down Expand Up @@ -157,8 +162,8 @@ export const paramParser: Record<keyof typeof paramTypes, paramParserDefinition>
const coordinate = matchResults[i * 3];
const operator = matchResults[i * 3 + 1];
const offset = matchResults[i * 3 + 2] ?? 0;
const offsetValue = parseInt(offset);
let coordinateValue = coordinate == "~" ? playerPosition[i] : parseInt(coordinate);
const offsetValue = Number(offset);
let coordinateValue = coordinate == "~" ? playerPosition[i] : Number(coordinate);
if (operator == "-") {
coordinateValue -= offsetValue;
} else {
Expand All @@ -168,7 +173,7 @@ export const paramParser: Record<keyof typeof paramTypes, paramParserDefinition>
}
return new ParseInfo(Vector3Utils.fromArray(parsedCoordinates as any), j);
},
regex: new RegExp(/^(?:-?\d+|~)\S*$/),
regex: new RegExp(/^(?:-?(?:\d+(?:\.\d*)?|\.\d+)|~)\S*$/),
regexError: "不是坐标格式",
},
flag: {
Expand Down Expand Up @@ -212,4 +217,4 @@ export const paramParser: Record<keyof typeof paramTypes, paramParserDefinition>
};

const TOKEN_SPLIT_REGEX = /[^~\s)]+|~[^\s~]*/g;
const TOKEN_REGEX = /^(-?\d+|~)(?:(\+|-)?(\d+))?$/;
const TOKEN_REGEX = /^(-?(?:\d+(?:\.\d*)?|\.\d+)|~)(?:(\+|-)?(\d+(?:\.\d*)?|\.\d+))?$/;
10 changes: 6 additions & 4 deletions src/Command/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ export class CommandParser {
if (command && command.isClientCommand) return chatOpe.skipsend;
if (!command || (command.isAdmin && !isAdmin(player))) {
if (!this.manager.testMode)
player.sendMessage([
{ text: "§c" },
{ translate: "commands.generic.unknown", with: [name] },
]);
player.sendMessage({
rawtext: [
{ text: "§c" },
{ translate: "commands.generic.unknown", with: [name ?? ""] },
],
});
return chatOpe.cancel;
}
//命中,解析命令
Expand Down
11 changes: 7 additions & 4 deletions src/Form/commonForm/InputForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CommonFormData, TextType } from "./commonFormInterface";
import { BaseField, FieldParseError, ValueField } from "./InputFormFields";

export interface InputFormArgs extends contextArgs {
fields?: ValueField<any>[];
fields?: BaseField[];
}

/**
Expand Down Expand Up @@ -86,7 +86,8 @@ export class InputForm<U extends InputFormArgs, TResult = any> implements SAPIPr
field.build(form, t);
}

args.fields = fields.filter((f) => f.isValueField) as ValueField<any>[];
// 保留全部字段(含 label/divider 等展示字段),与 formValues 槽位对齐
args.fields = fields;

return form;
}
Expand All @@ -97,7 +98,7 @@ export class InputForm<U extends InputFormArgs, TResult = any> implements SAPIPr
return;
}

const fields = ctx.args.fields as ValueField<any>[];
const fields = ctx.args.fields as BaseField[];
const values = res.formValues;
const t = translator.createPureFor(ctx.player);

Expand All @@ -114,7 +115,9 @@ export class InputForm<U extends InputFormArgs, TResult = any> implements SAPIPr

// 1. 字段级解析与基础验证
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const field = fields[i] as ValueField<any>;
// 展示字段(label/divider 等)占据 formValues 槽位但不产生输入值,跳过
if (!field.isValueField) continue;
const rawValue = values[i];

try {
Expand Down
10 changes: 9 additions & 1 deletion src/Form/formManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Player, ScriptEventCommandMessageAfterEvent, system } from "@minecraft/server";
import { FormRejectError } from "@minecraft/server-ui";
import { LibConfig } from "../Config";
import { ScriptEventBus, intervalBus } from "../Event";
import { LibErrorMes, getPlayerById } from "../func";
Expand Down Expand Up @@ -78,7 +79,14 @@ export class FormManagerClass {
return;
}
const builtForm = await form.builder(context.player, context.args);
const response = await builtForm.show(player);
let response;
try {
response = await builtForm.show(player);
} catch (err) {
// A rejected form is expected when a player closes it or leaves the game.
if (err instanceof FormRejectError) return;
throw err;
}

await form.handler(response, context);
this._handleShow(context);
Expand Down
61 changes: 58 additions & 3 deletions test/command/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// =====================================================
// Command Parser - Comprehensive Vitest Tests
// =====================================================
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// ─── Hoisted helpers: classes shared by mock and tests ─
const { MockPlayerClass, MockItemType, MockBlockType, MockEntityType, MockEntity } =
Expand Down Expand Up @@ -171,6 +171,7 @@ import { paramParser } from "../../src/Command/parser/ParamTypes";
import { Command } from "../../src/Command/commandClass";
import { ParseInfo, ParseError, ParamObject } from "../../src/Command/interface";
import { NativeCommandParser } from "../../src/Command/parser/nativeParser";
import { LibConfig } from "../../src/Config";
import {
ItemType,
GameMode,
Expand Down Expand Up @@ -315,15 +316,15 @@ describe("paramParser - 参数级解析单元测试", () => {
expect((result as ParseInfo).value).toBe("hello");
});

it("应支持带引号的字符串", () => {
it("应去除字符串外层的引号", () => {
const result = paramParser.string.parser(['"hello world"'], {
player: mockPlayer as any,
param: { name: "msg", type: "string" },
paramStrings: ['"hello world"'],
index: 0,
});
expect(result).toBeInstanceOf(ParseInfo);
expect((result as ParseInfo).value).toBe('"hello world"');
expect((result as ParseInfo).value).toBe("hello world");
});
});

Expand Down Expand Up @@ -439,6 +440,18 @@ describe("paramParser - 参数级解析单元测试", () => {
expect(info.value).toEqual({ x: 105, y: 64, z: 190 });
});

it("应解析带小数的绝对和相对坐标", () => {
const player = new MockPlayerClass({ location: { x: 100, y: 64, z: 200 } });
const result = paramParser.position.parser(["1.5"] as any, {
player: player as any,
param: { name: "pos", type: "position" },
paramStrings: ["1.5", "~0.25", "-2.75"],
index: 0,
});
expect(result).toBeInstanceOf(ParseInfo);
expect((result as ParseInfo).value).toEqual({ x: 1.5, y: 64.25, z: -2.75 });
});

it("缺少坐标时返回 ParseError", () => {
const result = paramParser.position.parser(["1"] as any, {
player: new MockPlayerClass() as any,
Expand Down Expand Up @@ -610,6 +623,10 @@ describe("CommandParser - 集成测试", () => {
vi.mocked(world.getAllPlayers).mockReturnValue([mockPlayer]);
});

afterEach(() => {
LibConfig.isHost = false;
});

function createSimpleCmd(name: string, params: ParamObject[]) {
return new Command(name, "测试命令", false).addParamBranches([params]).setHandler(handlerSpy);
}
Expand Down Expand Up @@ -640,6 +657,12 @@ describe("CommandParser - 集成测试", () => {
expect(handlerSpy).toHaveBeenCalledWith(mockPlayer, { msg: "hello" });
});

it("应去除命令输入中 string 参数的外层引号", () => {
const cmd = createSimpleCmd("test", [{ name: "msg", type: "string" }]);
parser.parseSubCommand(cmd, ['"hello world"'], mockPlayer);
expect(handlerSpy).toHaveBeenCalledWith(mockPlayer, { msg: "hello world" });
});

it("应正确解析 enum 参数", () => {
const cmd = createSimpleCmd("test", [
{ name: "dir", type: "enum", enums: ["up", "down"] },
Expand Down Expand Up @@ -669,6 +692,17 @@ describe("CommandParser - 集成测试", () => {
});
});

it("未知命令应发送 RawMessage,而不是导致 ScriptAPI 类型转换异常", () => {
LibConfig.isHost = true;
parser.parseCommand("missing", mockPlayer);
expect(mockPlayer.sendMessage).toHaveBeenCalledWith({
rawtext: [
{ text: "§c" },
{ translate: "commands.generic.unknown", with: ["missing"] },
],
});
});

// ─── 可选参数 ───────────────────────────────────
describe("可选参数 (optional)", () => {
it("应正常解析带可选参数的完整输入", () => {
Expand Down Expand Up @@ -1344,6 +1378,27 @@ describe("边缘案例", () => {
}) as any);
});

it("Command.fromObject 不应改写调用方的参数定义", () => {
const definition: any = {
name: "immutable",
explain: "测试",
paramBranches: [
{
name: "facing",
type: "flag",
branches: [[{ name: "pos", type: "position" }]],
},
],
};

Command.fromObject(definition);

expect(definition.paramBranches[0].branches).toEqual([
[{ name: "pos", type: "position" }],
]);
expect(definition.paramBranches[0].subParams).toBeUndefined();
});

it("无参数命令应正确工作", () => {
const cmd = new Command("test", "测试", false, handlerSpy);
parser.parseSubCommand(cmd, [], mockPlayer);
Expand Down
Loading
Loading