Writing an In-Game Debug Console

The time came to write a debug console for my game engine, to provide a means of monitoring and affecting the state of the game from an intuitive in-game prompt. This boils down to parsing of given commands, and the visual side of things. Here's an end result.

I'm going to focus on how the parsing of commands and arguments is implemented. Going in I didn't have any particularly complex requirements, but a basic need is having input commands and arguments for those commands, and sometimes having those arguments be other commands. Take this example line I want to execute:

entity 48 set_pos 15 162 342

We have a command "entity", which wants an argument of the ID of the entity to affect. We then have another command set_pos to be executed for the selected entity, and that requires its own arguments.

That's the result we'll get to. Let's start more simply, by writing a simple "print" function. It works like this:

static int print_func(char *args) {
    /* We'll validate arguments in other examples below. */
    printf("%s\n", args);
}

...

CommandParser<> parser;
parser.RegisterCallback("print", &print_func);
parser.Parse("print \"Hello world!\"");

The first thing you might notice is that CommandParser is a templated class. The template parameters are for the callback function to be given in addition to the char *args, which is important later.

The registered callbacks all have to take that char pointer. It's non-const so that it can be manipulated, such as by passing it through as a reference to some handy functions that will validate arguments.

struct ParseArg {
    static bool Long(long *ret, char **args);
    static bool Double(double *ret, char **args);
    static bool String(std::string *ret, char **args);
    static int Count(const char *args);
};

Count will count the number of arguments it suspects the given argument string contains (which could be wrong for some use cases - for example '8.f' will be considered a valid string input even though the user might be accidentally providing a number with unsupported float notation). The rest of the functions return a boolean indicating whether the input was valid and the *ret pointer has been assigned. On success the functions push the char **args to the next argument. On failure, the char** is unchanged. This allows some nifty argument validation.

static int print_vec3(char* args) {
    double x, y, z;
    int arg_count = 0;

    if(ParseArguments::Count(args) != 3) {
        printf("Error: Expected three arguments\n");
        return -1;
    }

    if((++arg_count && ParseArg::Double(&x, &args) &&
        ++arg_count && ParseArg::Double(&y, &args) &&
        ++arg_count && ParseArg::Double(&z, &args)) == false)
    {
        printf("Error parsing argument %d\n", arg_count);
        return -1;
    }

    printf("%f %f %f\n", x, y, z);
    return 0;
}

Since the string pointer is unaffected by failure, there's the option to take arguments out of order and do fun things. I'm also experimenting with having ParseArg hold a string for the last error to report back things like mismatched or unescaped quotes in strings, out of bounds numbers, or excessive punctuation in doubles.

Back to the example at the top:

entity 48 set_pos 15 162 342

We're going to want to take in the command entity with the argument 48, and then treat set_pos as a command with the arguments 15 162 342. We're also going to want the argument 48 passed through to the next function, so we know which entity the function is supposed to deal with. Here's the implementation:

static int entity_func(char *args);
static int entity_set_pos_func(uint64_t id, char *args);

...

CommandParser<> main_parser;
CommandParser<uint64_t> entity_parser;
main_parser.RegisterCallback("entity", &entity_func);
entity_parser.RegisterCallback("set_pos", &entity_set_pos_func);

The main entity handling function can then validate that the first argument of a given line is a valid entity ID, and then can pass that ID through to the entity_parser with the remainder of the argument string ("set_pos ..."). This command chaining allows for a great deal of expressiveness, without too much complexity.

The return values are all integers. For now I'm using these to return a negative value on unhandled failure, or '0' when the return value handled by its callback.

const char *cmd = "unregistered_command";

if(parser.ParseCommand(cmd) < 0)
    printf("Problem with command '%s'\n", cmd);

This means the callbacks can handle printing their own more specific error messages themselves if need be - which is better for the visual console.