Skip to content

Generalize and unify commands handling #2318

@bagggage

Description

@bagggage

Problem

Now, every command (/time, /tp, /kill, ...) parses arguments on its own, leading to duplicated implementations and sometimes even inconsistent behavior for the same things (#2310). Each command can also format and print error messages however it wants, with no standardized behavior.

Solution

To fix this, we could implement a shared CommandCaller that handles this logic. It would take the entire string after /, find the appropriate command by name, retrieve metadata about its arguments, and parse them according to their declared types. This approach would allow automatic generation of error messages for invalid argument formats and significantly simplify the execute implementations for each command.

It would also make refactoring new code easier, as developers adding a new command wouldn’t need to “reinvent” their own argument parser or search through other commands to see how parsing is implemented elsewhere.

To achieve this, I propose the following approach:

  • Command arguments are declared as a separate struct.
  • The execute function receives this struct as its parameter.
  • CommandCaller parses arguments using reflection via @typeInfo.
pub const Arguments = struct {
    x: usize,
    y: usize,
    z: usize,
    player: []const u8,
};

pub fn execute(args: *const Arguments) void {
    // ...
}

CommandCaller obtains all information about arguments through reflection: @typeInfo(MyCommand.Arguments), iterates over all fields of the struct, and parses each according to its type. We can predefine parsing rules for different types. If a type isn’t supported, we simply emit a @compileError.

For greater flexibility, users could define custom types with a parse function that may return an error. The parser would invoke this function for custom types and, if it fails, display the error name in chat. For example, we could create a special Coordinate type enabling commands like /tp to accept either ~ or a numeric value as coordinate:

pub const Coordinate = struct {
    value: ?isize = null,

    pub fn parse(str: []const u8) !Coordinate {
        if (str.len == 1 and str[0] == '~') return .{};
        return .{ .value = try std.fmt.parseInt(isize, str, 10) };
    }
};

We could also add something like printUsage to CommandCaller to automatically generate a usage message, and add a pub const description: []const u8 = "..."; field to each command to provide a command description that CommandCaller could then use.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementa new feature or improvementrefactorshuffle code aroundundecideda final decision has not been made whether or how this enhancement should be implemented

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions