I mentioned a Discord bot in the last post, and I wanted to cut out all the Star Citizen-ness of it (as much as I can, really) and talk about doing it.
Node is a popular platform for coding Discord bots (hereafter referred to just as “bots”) because it’s pretty ubiquitous and overall not the most complex platform to code for. It has its conventions that must be mastered, but a bot isn’t all that complex in itself, either.
At its most basic, a bot listens to all traffic on Discord, although it’s mainly ignored unless we tell it to listen for something specific, like a “!” command prefix. When the bot hears “![command]”, it will parse it against its library of actions, find one that matches, and Do That Thing. If there are arguments provided, it uses those to Do That Thing Properly And/Or Better. Then it returns something like a canned response or a bit of formatted information.
My SADI bot has big things in store. Big things. But she’s starting out small: accepting the name of a ship from Star Citizen and looking it up on the startcitizen-api.com website. This is a pretty simple maneuver in most cases: there are clearly defined endpoints for the API, and the data is returned in JSON format so it’s easy to parse (as Node and the discord.js library are, naturally, using javascript — or rather ECMAScript, from which javascript is derived).
As always, the macroscopic view is a lot rosier than the microscopic view. Case in point: dealing with search functionality is a pain the ass. Here’s the overview:
To search for a ship, the user should be able to enter the command
!sadi ship constellation
The bot will parse that: !sadi wakes the “SADI” bot module to handle the ship command with the constellation argument. It’s not super-sophisticated. When the ! is encountered, it’s stripped off and the “parser” element — “sadi”, which is a command library — is loaded. We pass the rest of the command string over to the “sadi” module, knowing that the first argument is always going to be the actual command — ship, which means we want ship information — and the second in this case is going to be “a name of a ship”. We split everything into an array based on spaces, which causes problems because we can’t search for “constellation andromeda” without asking the user to right-think into using something like underscores instead of spaces, and that’s just creating work for ourselves and our users.
Searching for “Constellation” on the API should be easy, but here’s the problem: this API doesn’t seem to do partial searches. We can search by name, but it has to be the whole name. That means passing “Constellation” isn’t going to cut it, because there’s the Andromeda, Phoenix, Aquilla, and Taurus entries, and searching just for “Constellation” brings back nothing. We can’t (or won’t) demand a full-name search because how many people are going to know to search for an F7C Hornet or an Aurora MR specifically?
My solution was to take the whole API ship output (which can be had by omitting any search criteria) and parsing out just the names. Then, I had to split them by the whitespace character into “model” and “variants”. Here, “Constellation” is the model, and “Andromeda”, “Phoenix”, “Aquila” and “Taurus” are varients. Not all ships have variants, but that’s OK. Some — like the goddamn Hornets — have their varients as prefixes, and worse: have suffixes like “Ghost” and “Tracker”.

This allows for searching by model. “Constellation” will allow me to pull out a complete name, but here’s the catch: I have to decide which variant to return. To do this, I start each new model match with a variant index of 0. That will bring back the first variant — “Constellation Aquila”. If the next search is also “Constellation”, then we cycle the index to the next value and get the next variant — ” Constellation Andromeda”. This continues until we’ve exhausted the variants, at which point we loop back to 0.

Well, that’s nice, but considering that the Connie line has several variants, wouldn’t it be easier to search for “Taurus” or “Aquila”? The answer is yes, and we do that by cycling through the models and if we don’t get a match on a model, we look at its variants. If we have a match at the variant level, then we have the model already, so we can concatenate our strings to submit to the search. We don’t even need to deal with indexing the variant collections at this point.
The only obvious thing missing is what to do when the user enters “Constellation Andromeda”, right? Well, no, because there is too much variation and the space between the parts of the name would force the app to split it anyway. I cannot rely on the “model variant” pattern because of this and because — once again — the goddamned Hornet.
Although the Hornet is the “model”, the “variants” are provided as suffixes like “F7C Hornet” or “F7A Hornet”. Searching for “hornet”, I have to be able to pull the Hornet as the model, the F7C as the variant, and then swap them to send “F7C Hornet” to the API, whereas normally it’s the reverse — “model variant”. Making matters worse is that there are a few Hornet variations that have names after “Hornet” — “F7C-R Hornet Tracker“, and “F7C-S Hornet Ghost“. There’s even the obnoxious “F7C-M Super Hornet Heartseeker“, which gives me a fit. To deal with this, I added these suffixes onto the variant prefixes with a pipe |. This means that I have to deal with Hornet searches as their own beast, swapping the variant and the model, and then slicing the variant if there’s a pipe and adding that to the end of the search string. Thing is, it works!
Is this the best approach? Well, it’s an approach. It would be easier if the API allowed partial searches because then I wouldn’t need to maintain my own crib sheet; I could download a subset of matching data via partial criteria and parse through that to find a more exact match or to store in memory and cycle through using the last index method. The biggest caveat of this method is that when CIG adds new ships — like the Carrack, the C8 and C8X Pisces (ugh), and the Mole, which are on deck currently — I’ll have to hand-add them to this crib list or re-run my full API parser to rebuild the list. Thankfully names of ships don’t change