From 77b2e0f4917ea1897874d04f885ed20ebdea8682 Mon Sep 17 00:00:00 2001 From: r4isen1920 Date: Sun, 28 Sep 2025 20:48:38 +0800 Subject: [PATCH 1/2] Feat: auto-register custom command Enum-type params This commit implements automatic registration of enum parameter sets for custom commands by detecting enum types in mandatory and optional parameters and invoking registerEnum as needed. --- __tests__/customCommand.test.ts | 60 ++++++++++++++++++++----- docs/customcommand.md | 25 +++++++++-- setupTests.ts | 4 ++ src/customCommandRegistry.ts | 77 +++++++++++++++++++++++++-------- 4 files changed, 134 insertions(+), 32 deletions(-) diff --git a/__tests__/customCommand.test.ts b/__tests__/customCommand.test.ts index be2de65..e05a2ec 100644 --- a/__tests__/customCommand.test.ts +++ b/__tests__/customCommand.test.ts @@ -53,20 +53,60 @@ describe("CustomCmd decorator", () => { expect((cmdInstance as any).run).toHaveBeenCalled(); }); - test("throws when run is missing", async () => { + // @CustomCmd + // class NoRunInvalid { + // readonly name = "test:norun"; + // readonly description = "no"; + // readonly permissionLevel = CommandPermissionLevel.Any; + // } +}); + +describe("CustomCmd enum auto-registration", () => { + test("registers enums from mandatory parameters", async () => { const mod = await import("../src/customCommandRegistry"); const { CustomCmd, registerAllCustomCommands } = mod; - class NoRun { - readonly name = "test:norun"; - readonly description = "no"; - readonly permissionLevel = CommandPermissionLevel.Any; + // Provide a mock global enum type identifier to simulate runtime + (globalThis as any).CustomCommandParamType = { Enum: "enum" }; + + class EnumCmd { + readonly name = "test:enum"; + readonly description = "enum test"; + readonly permissionLevel = 0; // Any + readonly mandatoryParameters = [ + { name: "test:pos", type: "enum", values: ["a", "b"] }, + ]; + static run = jest.fn(); } - CustomCmd(NoRun as any); + CustomCmd(EnumCmd as any); - const registry = { registerCommand: jest.fn() } as any; - expect(() => registerAllCustomCommands(registry)).toThrow( - /has no run method/ - ); + const registry = { registerCommand: jest.fn(), registerEnum: jest.fn() } as any; + registerAllCustomCommands(registry); + expect(registry.registerEnum).toHaveBeenCalledWith("test:pos", ["a", "b"]); + }); + + test("deduplicates enum registrations across mandatory/optional", async () => { + const mod = await import("../src/customCommandRegistry"); + const { CustomCmd, registerAllCustomCommands } = mod; + (globalThis as any).CustomCommandParamType = { Enum: "enum" }; + + class DuplicateEnumCmd { + readonly name = "test:dup"; + readonly description = "dup enum"; + readonly permissionLevel = 0; + readonly mandatoryParameters = [ + { name: "test:shared", type: "enum", values: ["x", "y"] }, + ]; + readonly optionalParameters = [ + { name: "test:shared", type: "enum", values: ["x", "y"] }, + ]; + static run = jest.fn(); + } + CustomCmd(DuplicateEnumCmd as any); + + const registry = { registerCommand: jest.fn(), registerEnum: jest.fn() } as any; + registerAllCustomCommands(registry); + expect(registry.registerEnum).toHaveBeenCalledTimes(1); + expect(registry.registerEnum).toHaveBeenCalledWith("test:shared", ["x", "y"]); }); }); diff --git a/docs/customcommand.md b/docs/customcommand.md index 14f7e06..399da57 100644 --- a/docs/customcommand.md +++ b/docs/customcommand.md @@ -1,6 +1,6 @@ -# CustomCmd – Custom Command decorator +# `CustomCmd` -Attach `@CustomCmd` to a class implementing the Minecraft `CustomCommand` shape. On startup, Stylish will instantiate and register it with the runtime's `customCommandRegistry`, wiring your `run` handler. +Attach `@CustomCmd` to a class implementing the Minecraft Script API `CustomCommand`. The class must either have a static or instance `run` method which will be invoked when the command is ran. ## Example @@ -29,5 +29,22 @@ class HelloCommand { } ``` -Decorated classes will be registered with the `CustomCommandRegistry` when -`init()` executes. \ No newline at end of file +Decorated classes will be registered with the `CustomCommandRegistry` when `init()` executes. + +## Enum parameter registration + +If your command defines `mandatoryParameters` or `optionalParameters` with entries of type `Enum` that include a non-empty `values` array, stylish will automatically invoke `customCommandRegistry.registerEnum(name, values)` the first time it sees each enum name. Duplicate enum names (even if repeated across mandatory/optional arrays) are de-duplicated. + +Example: + +```ts +readonly mandatoryParameters = [ + { + name: "example:mode", + type: CustomCommandParamType.Enum, + values: ["on", "off"] + } +]; +``` + +No additional manual startup code is required for enum registration as it's built for streamlined use. diff --git a/setupTests.ts b/setupTests.ts index 4acf931..2f17c0a 100644 --- a/setupTests.ts +++ b/setupTests.ts @@ -8,6 +8,10 @@ jest.mock('@minecraft/server', () => { Admin: 2, Owner: 3, }, + CustomCommandParamType: { + // Provide a symbolic value for Enum parameters; tests also accept string 'enum' + Enum: 'enum' + }, Direction: { Down: 'Down', East: 'East', diff --git a/src/customCommandRegistry.ts b/src/customCommandRegistry.ts index df1b4d1..e2b478f 100644 --- a/src/customCommandRegistry.ts +++ b/src/customCommandRegistry.ts @@ -1,23 +1,38 @@ -import type { - CustomCommand, - CustomCommandRegistry, -} from "@minecraft/server"; +import { CustomCommandParamType, type CustomCommand, type CustomCommandParameter, type CustomCommandRegistry } from "@minecraft/server"; import { registerInstanceEventHandlers } from "./eventRegistry"; /** - * Constructor type for Custom Commands. Constructors must not have parameters. + * Definition for each parameter expected by the custom + * command. */ -export type CustomCommandCtor = new () => CustomCommand & Record; +export interface StylishCommandParameter extends CustomCommandParameter { + /** Optional set of enum values (required when type === Enum). */ + values?: readonly string[] | string[]; +} +/** + * Define the custom command, including name, permissions, and + * parameters. + */ +export interface StylishCustomCommand extends CustomCommand { + mandatoryParameters?: StylishCommandParameter[]; + optionalParameters?: StylishCommandParameter[]; +} + +/** + * Constructor type for Custom Commands. Constructors must not have parameters. + */ +export type CustomCommandCtor = new () => StylishCustomCommand & Record; const registry: CustomCommandCtor[] = []; /** * Decorator – attach to each Custom Command class to auto-register it. */ -export function CustomCmd(ctor: T) { - registry.push(ctor); +export function CustomCmd(ctor: T): T { + registry.push(ctor as CustomCommandCtor); + return ctor; } /** @@ -30,20 +45,46 @@ export function registerAllCustomCommands( const instance = new Ctor(); registerInstanceEventHandlers(instance); - let runFn: ((...args: any[]) => any) | undefined; - const maybeStaticRun = (Ctor as any).run; - if (typeof maybeStaticRun === "function") { - runFn = maybeStaticRun.bind(Ctor); - } else if (typeof (instance as any).run === "function") { - runFn = (instance as any).run.bind(instance); - } - if (!runFn) { + // Look for either a static or instance run method + const hasStaticRun = typeof (Ctor as any).run === "function"; + const hasInstanceRun = typeof instance.run === "function"; + if (!hasStaticRun && !hasInstanceRun) { throw new Error( - `Custom command ${Ctor.name} has no run method. Define a static or instance run(origin, ...args).` + `Custom command ${Ctor.name} has no run method. A static or instance run(origin, ...args) is required.` ); } + const runFn = hasStaticRun + ? (Ctor as any).run.bind(Ctor) + : instance.run.bind(instance); + + + // Auto-register any enum parameter sets (mandatory or optional) + const enumNames = new Set(); + /** + * @private + */ + const collect = (arr?: StylishCommandParameter[]) => { + if (!Array.isArray(arr)) return; + for (const def of arr) { + try { + if (def.type === CustomCommandParamType.Enum && + Array.isArray(def.values) && + def.values.length > 0 && + !enumNames.has(def.name) // ignore duplicates + ) { + enumNames.add(def.name); + customCommandRegistry.registerEnum(def.name, def.values); + } + } catch (e) { + console.warn(`Failed to register enum parameter for command ${instance.name}:`, e); + } + } + }; + collect(instance.mandatoryParameters as StylishCommandParameter[] | undefined); + collect(instance.optionalParameters as StylishCommandParameter[] | undefined); + - customCommandRegistry.registerCommand(instance as any, runFn as any); + customCommandRegistry.registerCommand(instance, runFn); } } From 8820072dff02546aa6808e6fad61bc57249b7c64 Mon Sep 17 00:00:00 2001 From: r4isen1920 Date: Sun, 28 Sep 2025 21:05:36 +0800 Subject: [PATCH 2/2] Docs: update main page `README.md` --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 824a508..2792910 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Stylish is a decorator library aimed at simplifying development for the Minecraf - `@ItemComponent` – automatically registers a custom item component. - `@BlockComponent` – automatically registers a custom block component. +- `@CustomCmd` - automatically registers a custom command. - `@BindThis` – binds a method to its instance when accessed. - `@OnStartup` – runs decorated methods when the pack starts. - `@OnWorldLoad` – runs decorated methods when the world is loaded. @@ -23,7 +24,7 @@ npm install @bedrock-oss/stylish ``` 2. Enable decorators in your `tsconfig.json`: -```jsonc +```json { "compilerOptions": { "experimentalDecorators": true @@ -72,6 +73,7 @@ See the [`docs`](docs/) folder for details on decorators: - [BindThis](docs/bindthis.md) - [ItemComponent](docs/itemcomponent.md) - [BlockComponent](docs/blockcomponent.md) +- [CustomCmd](docs/customcommand.md) - [Events](docs/events.md)