I decided to title these articles a bit differently. So far I’ve intervowen development content, and actual news/updates about SCHNAIL – the time has come to differentiate these kind of contents.
There has been two major features that I’ve been working on recently, both of them had their unique challenges. These are two of the for major features I consider necessary to move forward onto open beta. I’ll reveal all of these as I delve into the details.
The first of those were automating bot testing. The basic problem is that if you write a bot, and upload it, there is no guarantee that it will perform, or even run on the end user’s machine. And that’s a problem. Manual testing is not a scalable option – currently we have a manageable number of bots, but we should always keep scaling in mind. The problem statement is that I need an automated way to ensure that a bot runs in the environment.
This is not even evaluating the bot’s strength, or quality. My first thought was that I will run games against the built-in AI, and if the bot defeats those, I consider it a good enough one. But there are exceptions to that rule: a very good 4 Pool bot might be a good one. Therefeore I lowered a bar, the bot in question just needs to defeat a “sitting duck” opponent. On SCHNAIL, every bot has its own set of allowed maps, so one game per map for verification is a good starting point.
First things first: How do I run SC games in an automated way? BWHeadless was my first thought, after all, the SCHNAIL client uses that as well. It is a great tool for running games without graphical output. The problem is that the server environment for the project is Linux, and bwheadless needs to use wine, and there are a few issues reported with it. I didn’t really get into that, but kept looking for tools.
I found SC-docker by Bytekeeper. This is the backbone used to run BASIL, a headless ladder for bot evaluation. BASIL has been running for quite a while, so that was proof enough for me. I installed it (Well, Bytekeeper did it for me), then I got to work. Everything was going well, I manually created bot entries, and ran them against each other, but at one point, it suddenly stopped working. Seemingly nothing changed. Sc-docker needs another sudoer user, so we created one, and I did the following:
su - <user>
scbw.play <params>
scbw.play: command not found
After a considerable amount of frustration, Bytekeeper informed me that the fix was very simple. Instead of su
I need to use su -l
.
I began working on the server to set up the environment. Just copying the files to the right place is kind of a hassle, but ok, that was handled. Then nothing was running, because of course it didn’t. It turned out that permissions were lacking, and sc-docker couldn’t write the result files. Okay, let’s have permissions.
I naively thought that Linux got this, so I’ll just use chmod on the files with a process. Simple, yet elegant. I did something like this:
ProcessBuilder pb = new ProcessBuilder("chmod 777 " + filename);
pb.start();
It didn’t really do anything, and after some googling, I found out that ProcessBuilder does not play nice with Linux environments. It can be beaten into submission, and I’m not one to shy away from excess violence, but maybe there is another way. Turns out the following way is more stable.
Runtime.getRuntime().exec("chmod 777" + filename);
Of course, file permissions still didn’t change at all, so I had to investigate what the hell is going on now. I followed this Stack Overflow post, and tried it with directly using Java objects, something along the lines of:
file.setExecutable(true);
File.setWritable(true);
Guess what happened.
I actually couldn’t find out the exact reason, but I didn’t exactly want to. Usually in these cases I try out all the ways I can think (and Google) of, and stick with the one that works. It wasn’t any different now, so the final solution that working was the following:
Set<PosixFilePermission> perms = Files.readAttributes(Paths.get(next.getPath()), PosixFileAttributes.class).permissions();
perms.add(PosixFilePermission.OWNER_WRITE);
perms.add(PosixFilePermission.OWNER_READ);
perms.add(PosixFilePermission.OWNER_EXECUTE);
perms.add(PosixFilePermission.GROUP_WRITE);
perms.add(PosixFilePermission.GROUP_READ);
perms.add(PosixFilePermission.GROUP_EXECUTE);
perms.add(PosixFilePermission.OTHERS_WRITE);
perms.add(PosixFilePermission.OTHERS_READ);
perms.add(PosixFilePermission.OTHERS_EXECUTE);
Files.setPosixFilePermissions(Paths.get(next.getPath()), perms);
Checked it manually, permissions were set, sc-docker was running fine. I started to work on automating sc-docker commands. The first approach was just running them in parallel, because why not. I just ran unnamed random games in parallel vs. Marine Hell, the punching bag bot. This approach almost always resulted in a timeout, even with giving the bots 10 minutes to run. At that point, I might as well just run the games in sequence, so I did just that. This was fine, it took between 15-30 minutes to evaluate a bot, but that is acceptable if it’s done automatically.
Marine Hell is a fine potato bot to play against, but for reasons mentioned above, I wanted a true sitting duck, a bot that doesn’t do anything at all. I quickly created a bot using JBWAPI (which is the best Java framework for bots right now, I think), and ran games against it.
These always timed out. Fortunately, I had some logs to look at.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
at bwapi.Game.init(Game.java:185)
at bwapi.EventHandler.operation(EventHandler.java:21)
at bwapi.Client.update(Client.java:239)
at bwapi.BWClient.startGame(BWClient.java:43)
at bwapi.BWClient.startGame(BWClient.java:25)
at exec.ExampleBot.run(ExampleBot.java:23)
at exec.ExampleBot.main(ExampleBot.java:27)
This baffled me a bit, as this bot literally did nothing.
The offending line in JBWAPI was this:
enemy = players[gameData.getEnemy()];
Turns out that this was an actual bug in JBWAPI, the game sometimes didn’t initialize the gameData at this point, so we got an exception. (I notified Yegers, and the bug is fixed since.)
I think I’ll just paste the conversation about the issue here. A professional discussion took place.
Okay, let’s just empty the replays folder before each evaluation.
In the meantime, I thought okay, I’ll just use a dll bot while the JBWAPI bug is fixed. Manually testing it confirmed that it works. So far I have worked with hardcoded game names, so I set up a generation scheme. Bots and games have guids identifying them, so I just used the botguid_mapguid scheme to create game names. It’s not really human-friendly, but it doesn’t need to be. I hope I don’t need to do anything with these games manually. They are also deleted after a certain point, since they are basically unit tests.
Yet again, all the games were crashing and I didn’t see any output. At this point, my jimmies were somewhat rustled. I ran the games manually on each map – they ran properly. I tried playing around with all the parameters to cause the issue, and eliminated them one by one. I wrote a spreadsheet with everything tried, the ranges of the variables, different bots and maps. Everything was run the same way when I did it manually, but when I triggered it from a server process, it failed.
The only variable I didn’t consider relevant, and thus didn’t test was the game name. Surely, it can’t be?
It was the issue. Turns out, 6 characters is the limit. I believe the exact technical reason for this is “fuck you, that’s why”.
Even now, sometimes the evaluation didn’t start. This was because some docker containers complained about name reuse, so I needed to issue a docker container prune -f
command.
As the jimmmy rustling further intensified, this turned out to be the last piece of the puzzle. The bot evaluation pipeline was finally working, and triggering on uploads. I tested it with different bots, and everything seemed to be fine – and finally, I was done with this shit desired functionality. This took about a week, and I was ready to move on to live in Nepal as a goat the next feature.
Custom hotkeys
When starting out with SCHNAIL, I didn’t think about this to be honest. But the platform’s target audience is gamers, and the top requested feature was to add custom hotkeys, like you can do with SC:R. The ways this feature request was conveyed was varying. And well, I have to keep in mind my target audience.
I began to analyse the problem. I was faintly aware of some work done in this area, because there was a period after SC2, but before SC:R when people wanted to do this. The most prominent example is the annoyance over pressing P for probes.
The starting point was this repo, which adds SC2 hotkeys into Brood War. It contains a guide that explains how it is done. The game files are stored in an MPQ format, which is a proprietary format for Blizzard, and it is not publicly specified. But smart and persistent people have decoded it in the past. (Going back as far as 2000). One of these MPQ files, patch_rt.mpq contains a file where are the strings describing hotkey descriptions for the game. First, I needed an MPQ editor, and I found a few.
The most popular it seems it’s Ladik’s MPQ editor. There is WinMPQ, but it is ancient, and there was PyMPQ, a part of the PyMS repo, which contains basically all the things you need to have for editing (modding) the game.
A quick note about that: Do NOT do this for your game instance that you use for online multiplayer. SCHNAIL is a closed environment, with its own rules. You can’t use it for ladder, or any kind of unfair play. We are making extra sure of that.
After some googling, I found out that I need to extract the rez\stat_txt.tbl file for editing, and convert that to some kind of parseable text. PyTBL was working fine for this purpose. There are othe TBL editors out there, but this was the most usable. I manually edited some strings using the PyTBL guide, and tried it out.
This was a promising start for once. It took approximately 12 minutes to realize that I can change any text in the game.
Of course, the meme possibilities were endless.
Moving on. The problem was that none of these files actually had a format I could use. I needed to keep track of the hotkey, original, and modified text, and some identifier for the text so I know what to replace exactly. Since the TBL files are compiled/decompiled in a deterministic way, just keeping track of the line numbers for the texts was sufficient for this.
But those line numbers I had to extract from the file, and I needed to create an object for each one of them. Also, create objects for the hotkey entries, and map them to units, and map those to subgroups, and races.
Two days, ~1200 lines of code, and a carpal tunnel later, I was ready to put all of these into a UI. I wanted something that is reminiscent of the SC:R hotkeys UI. I couldn’t really replicate it exactly, but that’s ok, I didn’t need to. This is the current (WIP) state:
It needs a lot of styling, and arranging, but the components are there, and they work. Well, the UI part does. Let’s circle back to TBL and MPQ editing.
As for TBL files, PyTBL works, but it has some requirements, notably a specific version of Python (2.7.8), and some libraries. If I want to use this, I need to make sure the user’s computer has these installed, and bundle the installer packages with the SCHNAIL setup. In general, I want to avoid relying on third-party tools as much as possible, and avoid having a lot of dependencies that way. In the end, I used another utility which is a standalone program.
Onto the MPQ editor’s. Ladik’s MPQ editor had a nice graphical interface, and command-line options too, and of course it didn’t work. I couldn’t identify the issue, if I added a file to the MPQ, StarCraft just derped out. WinMPQ worked, it had a command-line interface, but it was written sometime around the first world war. It had Visual Basic 4 as a dependency. Yet again, this was kiiinda good enough, but I wanted to search for other solutions.
I found JMPQ3. It seemed perfect, as it is a Java library that I can use. I started working with it, It extracted the TBL files I needed, I edited that from the UI, and saved it properly. I tested with WinMPQ, and the texts got changed properly. All that is left is putting it back to the MPQ file.
I think you can guess what happened next.
First, to properly handle MPQ files, you need a listfile. These are files that describe the archive’s content – fortunately, Brood War’s is readily available. Without a listfile in the archive itself, JMPQ couldn’t save the archive. Okay, no problem, I added the listfile (it has to be called “(listfile)”), and used that mpq by default. StarCraft didn’t complain about it, so why not. JMPQ didn throw any error this time and didn’t complain (too much).
But someone else did.
JMPQ has some three options when closing MPQ files (meaning save them). This is 6 different possibilities to try out, and none of them worked.
This was the end of the road. JMPQ has no other option I can manipulate, and with some trial and error, I found out that it has a problem inserting certain files into the archive – I suspect it has something to do with the file sizes.
This is the point where I’m at while writing this article. There are still options. First is to just use WinMPQ, but yet again, I can’t fix anything if that doesn’t work (Although I sincerely hope I don’t have to do any kind of MPQ editing after this). Also, forking JMPQ and fixing the bug in question is also possible, and a non-trivial amount of work. The best is maybe working on the basis of PyTBL/PyMPQ and creating our own library for this. That could be the best longterm solution for this. PyMS is already a great tool for modding, but having it available as a Java library too would be great, and useful in the future.
This has been a long journey of deconstructing StarCraft. The positive side effect is that we have all the tools available to make the absolute meme version of it.
Thanks for reading! If you liked this article, or want to help the SCHNAIL project, consider supporting us on Patreon! In the meantime, you can follow Making Computer Do Things on various channels, such as Twitter, YouTube, or even Twitch!