diff --git a/include/hammy/command.h b/include/hammy/command.h index b3393af..d134524 100644 --- a/include/hammy/command.h +++ b/include/hammy/command.h @@ -1,18 +1,64 @@ #ifndef HAMMY_COMMAND_H #define HAMMY_COMMAND_H -#include +#include +#include + #include +// A command handler. Runs either on the gateway thread (instant commands) or on +// a worker thread (deferred ones), so it must not assume either. client is +// whichever client is correct for the calling thread; use it and nothing else. +// +// Instant handlers send a fresh interaction response. +// Deferred handlers EDIT the already-deferred response, via hammy_job_reply() +// or discord_edit_original_interaction_response(). +// +// The handler does NOT own the job and must not destroy it. +typedef void (*hammy_command_fn)(const hammy_job_t* job, struct discord* client); + +// Plain old data. Every field points at static storage, so this struct owns +// nothing, copies freely, and needs no destructor - the vector can be created +// with a NULL destructor hook. struct hammy_command_t { - // TODO: add shit here - char empty; + const char* name; // Discord command name. Not owning; expected to be a literal. + const char* description; // Not owning. + + // Registration options, or NULL for none. Not owning; point at a static + // struct discord_application_command_options, e.g. + // + // static struct discord_application_command_option grid_opt_array[] = { + // { .type = DISCORD_APPLICATION_OPTION_STRING, .name = "locator", + // .description = "Maidenhead grid", .required = true }, + // }; + // static struct discord_application_command_options grid_opts = { + // .size = 1, .array = grid_opt_array + // }; + struct discord_application_command_options* options; + + hammy_command_fn handler; + + // true -> pure computation; answered inline on the gateway thread, no + // defer, no queue. Must complete in microseconds. + // false -> may block (HTTP, DB, file IO); deferred and queued to a worker. + // + // Marking a blocking command instant stalls the gateway and risks missed + // heartbeats. When unsure, leave it false. + bool instant; }; - -// Creates the command on the heap, returns NULL if failed. -hammy_command_t* hammy_command_create(); - -// Destroys a command. -void hammy_command_destroy(void* cmd); + +// The built-in command table. Returns a pointer to static storage; outCount +// receives the number of entries. Never NULL. +const hammy_command_t* hammy_command_builtins(size_t* outCount); + +// Linear lookup by name over a plain array. The table is small enough that +// anything cleverer is not worth the code. +// Returns NULL if not found. +const hammy_command_t* hammy_command_find(const hammy_command_t* commands, size_t count, + const char* name); + +// True if the command is safe to run inline on the gateway thread. +// Tolerates NULL so callers can skip a null check. +bool hammy_command_is_instant(const hammy_command_t* command); #endif diff --git a/src/hammy/command.c b/src/hammy/command.c index bdcf7ac..7a78ad4 100644 --- a/src/hammy/command.c +++ b/src/hammy/command.c @@ -1,24 +1,51 @@ +#include +#include + #include -hammy_command_t* hammy_command_create() { - hammy_command_t* cmd = (hammy_command_t*)malloc(sizeof(hammy_command_t)); - if (!cmd) { - return NULL; - } +// Handlers live in src/hammy/commands/. Declared here rather than in a header +// so adding a command touches exactly two places: its own .c file and this +// table. +void hammy_cmd_ping(const hammy_job_t* job, struct discord* client); - // TODO: set defaults - - return cmd; +// The command table. Pure data - no allocation, no lifetime, no destructor. +// Copying an entry into the bot's vector is a plain struct copy, and the vector +// may be created with a NULL destructor hook. +// +// Field order: name, description, options, handler, instant. +static const hammy_command_t hammy_builtin_commands[] = { + { "ping", "Check whether Hammy is alive and how fast it is responding", + NULL, &hammy_cmd_ping, true }, + + // Tier 0 commands go here as they land. All pure computation, so instant: + // { "grid", "Maidenhead locator conversions", &grid_opts, &hammy_cmd_grid, true }, + // { "morse", "Text to and from Morse code", &morse_opts, &hammy_cmd_morse, true }, + // { "band", "What band is a frequency in", &band_opts, &hammy_cmd_band, true }, + // + // Anything touching the backend API, a database or the network is NOT + // instant: + // { "call", "Look up a callsign", &call_opts, &hammy_cmd_call, false }, +}; + +const hammy_command_t* hammy_command_builtins(size_t* outCount) { + if (outCount) + *outCount = sizeof(hammy_builtin_commands) / sizeof(hammy_builtin_commands[0]); + + return hammy_builtin_commands; } -void hammy_command_destroy(void* cmd) { - if (!cmd) { - return; +const hammy_command_t* hammy_command_find(const hammy_command_t* commands, size_t count, + const char* name) { + if (!commands || !name) return NULL; + + for (size_t i = 0; i < count; i++) { + if (commands[i].name && strcmp(commands[i].name, name) == 0) + return &commands[i]; } - cmd = (hammy_command_t*)cmd; - - // TODO: Other things here eventually - - free(cmd); + return NULL; +} + +bool hammy_command_is_instant(const hammy_command_t* command) { + return command && command->instant; }