Last time I wrote one of these was LD51 "Every 10 seconds".
This post contains major spoilers for Himari. The game is better enjoyed spoiler-free.
Signal has been popping up in several recent theme votes and I always knew that should it would win, I would have to involve the Signal messenger. The idea is so obvious to me that I'm really surprised I haven't seen another similar game. A few people said they considered it as a joke and... I guess that makes my game a bit of a joke (and it did end up scoring 3.95 in Humor). Then again, I'm not right in the head; for LD49 "Unstable" I submitted a VirtualBox VHD with modified Debian Unstable, and don't even get me started on LD53 "Delivery" (spoiler: I will get started on my own).
How to make a Signal bot
Turns out, the Signal bot ecosystem is close to nonexistent. There is signal-cli-rest-api, which is a wrapper around the unofficial CLI client for Signal. The most popular frontend for this API seems to be signalbot which involves, inhales, Python. I spent half an hour fiddling with signalbot and couldn't figure out how to marry it with Flask, which was probably so much of a lost cause I can already imagine Python developers gently pressing their snake venom stained hands against their snake venom stained foreheads. "But", you interject, "pythons aren't ven-" but signal seems to be quite weak because I can't hear you. Thankfully, the rather primitive signal-rest-ts spared me from having to write my own even more primitive wrapper in a language that isn't Python.
That's step one. Step two is a vidya made in Godot, because a chatbot alone would barely qualify as a game. Step three is a simple REST API that will let Godot sync the state and send signals to the bot (no, this one wasn't on purpose). I list them in this order as if they were made in order, but they were all made simultaneously. A large portion of the bot was built with zero testing because I had to drive to a convenience store in another town to buy a phone number, and I only got round to that around Saturday 15:00 UTC.
There is also a secret step four which doesn't have a solid plan at the beginning and will have to be figured out on the go.
The bot isn't inherently remarkable in any way. It's really just the glue that makes it interesting.
How to connect the client
The Signal server stores the current state counter which tracks how far into the game you are, or at which question. For example, state -40 waits for you to enter your name and state 70 is when she says "This is getting a bit boring" as the lava starts approaching (the value is negative until the Godot client has been shared; at the same time, the Godot client will refuse to run with a negative state).
If that sounds simplistic and unscalable, well, it is–but this is a compo game. State management and synchronization is certainly the most bodged aspect of the project, but engineering a proper state tree would have taken more time than managing a big if-else chain and remembering to return; after every interaction; the scope is small enough for that to work, and if it works, it works. If I ever want to expand on the idea, it's just two days of work to throw out.
The server also exposes two HTTPS endpoints: for polling and for updating the state based on UUID, and the client polls the state around every two seconds. To prevent unexpected situations, the state can only go up; the only exception is the intro to the cavern, where Himari repeats her instructions if you get lost. And, lo and behold, that particular interaction turned out buggy in the end: if you go back to the starting point after having cleared the fog, she will say the lines again.
Overall, that's pretty simple, but it only covers a single variable. There is no endpoint to receive your favorite color, as I didn't want to expose an endpoint with any "meaningful" data (as meaningless as it is in reality).
How to make it personal
One major point of the game, and probably my favorite point, is that the build of Bunny sent to you by Himari is "tailor-made" based on your answers. This is the secret step four. I set out two requirements I had to satisfy:
- The download had to be a direct attachment sent by Himari through Signal, i.e. not a download link.
- This of course is the sole reason why I recommended Signal Desktop, which was a bit unwieldy, but this requirement was necessary for immersion.
- It could not involve any user interaction that would make it look like a non-personal build e.g. entering a code.
The easiest solution is to run Godot CLI and actually, truly, honestly, export a personalized build, but I didn't want to risk zooming past Signal's attachment size limit (which seems to be 100MB) and finding that out during the submission hour, so I was more comfortable with a downloader approach. The fastest way is to write a script, adjust it for the user, and pack it into an executable; in hindsight, I should have tried dotnet first. Bun builds are around 100MB; in hindsight, I should have tried something like Porffor. Bad decisions after bad decisions, and I found myself using pyinstaller in the end; I thought I had escaped Python, but life finds a way. This is the script, with $URL (windows/linux) and $STATE (data like your favorite color) getting adjusted at build time:
```py
dirs = PlatformDirs("LD59Bunny", "LD59")
if not os.path.exists("bunny"):
os.mkdir("bunny")
if not os.path.exists(dirs.userconfigpath):
os.makedirs(dirs.userconfigpath, exist_ok=True)
urllib.request.urlretrieve("$URL", "bunny.zip")
with zipfile.ZipFile("bunny.zip", "r") as zipref:
zipref.extractall("bunny")
with open(os.path.join(dirs.userconfigpath, "bunny.conf"), "w") as f:
f.write('$STATE')
```
As you can see, the downloader sneaks a config file into XDG_CONFIG_HOME/%LOCALAPPDATA%, and that's the transparent way to make the build "personal". The client learns the Signal ID from this config file, which lets it communicate with the server. The only variable that will change on the server side from then on is the state counter, and everything else is henceforth stored locally.
One major point of pain with Signal (or, at least, with the API) is that attachments can only be sent as octet streams, so the downloader had to be packed in a zip and automatically recognized as a zip; I couldn't send an exe and tell Signal "this is an exe". Or it's possible and I'm just dumb, which I am.
How to port a game to Windows
My pace was so decent that the game was basically ready by 23:00 UTC, and I spent the remaining two hours on the finishing touches. Part of the reason for this good pace was my initial focus on the systems; almost all of the work on the meta-game was done on Saturday, and Sunday afternoon/evening was the time to add content to the Godot game (the only major exception was the bunny sprite, which was originally intended to be one of two playable characters). When the submission hour arrived, believe it or not, I actually submitted the Linux build, tested it, and moved over to Windows. By that point I was technically free from the timer, as porting can be done at any point, but I wanted to get it over with.
Now, I have a bit of a history with Windows ports. When I used SFML, every Windows port was a little adventure that may or may not have kept me up till late morning. I thought moving to Godot would solve my porting woes, but then LD53 "Delivery" arrived and I made a game that involved a mobile companion app (parody of Just Eat); it was actually the spiritual predecessor to Himari. Turns out, Godot 4.0 .NET didn't support Android exports, and I learned that during the submission hour, so I had to speedrun rewriting the app in GDScript; thankfully, it was only around 100 lines, but that could have been a disaster. Now here we are, LD59; I would think Himari for Windows would only involve clicking the Windows export in Godot and adjusting the downloader's URL based on the OS, but turns out pyinstaller can't cross-compile. Of course it can't. What did I expect.
There were multiple potential fixes, they were all bad, and I eventually settled with running Python embeddable over wine, which miraculously ended up working with a few hiccups. In the meantime, I lost access to the VPS for around an hour, and I tried just about every possible troubleshooting method, only to find out I was SSH-ing to the wrong address. But that's the kind of adventure one can expect to have at 5:00. By 6:00, Monday, the Beautiful Super Genius Himari could build tailor-made experiences for both Linux and Windows, and by 10:00 I got a call from work.
How to reflect on this personal experience
This is the only section written after the results had gone live. I made sure to write out all the details two weeks ago, because I was sure I would have forgotten some of them otherwise.
Oddly enough, the game performed exactly as I expected/hoped for: with a decent-ish Overall and a medal for either Inno or Theme (I got both).

A post-mortem should end with some reflection for the future, but I tricked you (that is, if you're not accustomed to reading blogpost titles)–this isn't a post-mortem; it's a rambling.
I have nothing to reflect on. Himari was good, and this is the first time I've ever felt this way about my own LD game. I wouldn't change a damn thing about the actual game aspect of it, and the meta-game aspect was servicable enough, and, really, pretty one-of-a-kind, all things considered.
This was my first LD since LD55 "Summoning" when I was fully available over the weekend, and I got another silver. I'm hesitant to say "I'm just getting started" given the uncertain future of Ludum Dare, so I'll just say I have peaked.
I will delete Himari in a few days after uploading a gameplay video for "preservation". One reason for that is that I don't want to keep hosting this bodged server on a dedicated $6 droplet, and I also don't want to store any data for an extended time even if it's something as useless as Signal GUIDs. Maybe, if I think this through, I will re-launch her again sometime.
Actually, as I keep on rambling, I realize that the one thing that stood out this jam was that I wasn't in a huge hurry to make a playthrough video, because the game was pretty straightforward to clear (aside from the final jump and the final hidden corridor, but that's the finish line). Bagelseeker could have scored just as high in Innovation if people were actually able to play it, so that's something I can definitely say I learned from my past mistakes. The cycle is broken:
And also, if I make something like this again, I probably won't use Signal, which, funnily enough, was the whole point.
Other interesting post-mortems