Cubyz icon indicating copy to clipboard operation
Cubyz copied to clipboard

Generalize and unify commands handling

Open bagggage opened this issue 1 month ago • 4 comments

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.

bagggage avatar Nov 19 '25 16:11 bagggage

If you don't want to do this yourself. I would like to try this. Also with your now more extensive Issue I will close my other PR.

Wunka avatar Nov 19 '25 16:11 Wunka

Feel free to give it a try - I don’t mind, but I’d still wait to hear what others think about this idea.

bagggage avatar Nov 19 '25 16:11 bagggage

#1425

Argmaster avatar Nov 19 '25 18:11 Argmaster

Most of what you have described was already implemented and discussed, but it is not a priority to get it merged.

Argmaster avatar Nov 19 '25 18:11 Argmaster