Generalize and unify commands handling
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
executefunction receives this struct as its parameter. CommandCallerparses 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.
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.
Feel free to give it a try - I don’t mind, but I’d still wait to hear what others think about this idea.
#1425
Most of what you have described was already implemented and discussed, but it is not a priority to get it merged.