I took down 541 zerglings with 778 APM

Posted by

Yep, that's right! I'm a Starcraft legend!

OK, not really... I'm actually a big fat cheater. I wrote a little Chrome extension that clicks on the zerglings. It only took about 10 minutes to write the first version, but it was actually a bit of tweaking to get it to work well. The first version just clicked on zerglings as fast as it could, but the problem with that was, the game never actually finished! I killed the zerglings before they could do any damage.

So I tweaked it so that it lets a couple of zerglings run around and do some damage. But the problem there was, the APM actually ends up being kind of low. Then I noticed if you click anywhere in the body it counts as an action, so the final script looks like this:

// kill the zerglings
setInterval(function() {
  var zergs = document.querySelectorAll(
                ".zr_zergling_container");
  if (zergs.length < 6) {
    // let a couple of them run around so that
    // the game will actually end (eventually...)
    return;
  }
  for (var i = 0; i < zergs.length; i++) {
    var evnt = document.createEvent("MouseEvents");
    evnt.initMouseEvent("mousedown", true, true, window,
        0, 0, 0, 0, 0, false, false, false, false, 0, null);
    zergs[i].dispatchEvent(evnt);
  }
}, 50);

// this'll keep our APM up, 100ms == 600APM
setInterval(function() {
  var evnt = document.createEvent("MouseEvents");
  evnt.initMouseEvent("mousedown", true, true, window,
        0, 0, 0, 0, 0, false, false, false, false, 0, null);
  document.body.dispatchEvent(evnt);
}, 100);

Take that, zerg rush!

Using the App Engine Channel API from a Java client

Posted by

For my game, one of the features is real-time chat. Now, App Engine comes with a "Channel API" which is basically a means of executing comet-like "long polling". The API is really simple, yet it has one serious drawback: the client-side is JavaScript.

That means, if I wanted to include a Channel API client in my game, I would need to create a "hidden" WebKit frame, with a bit of JavaScript and then a bunch of wrappers to actually get the messages into my Java game client. What a pain in the butt!

There's a number of bugs and feature requests against the App Engine API to support more than just JavaScript clients (for example, this one for a Java client). Tom Parker has written a Java client that talks to the Channel API on the dev server, but it doesn't work against the production API (as far as I can tell anyway -- the production API is completely different and implemented in terms of the Google Talk chat client). I found a post, "Using Google Appengine Channel API with a Python Client" by Schibum which works against the production API, but it's written in Python (and it doesn't work against the dev server -- I'd like something which worked against both, obviously).

So over the weekend, I've been working on my own Java client. This client works against both the development server and production App Engine servers. I've taken inspiration from both implementations I linked to above. The code is not entirely self-contained, it requires the Appache HttpClient library, an implementation of JSONObject and it uses SLF4j for logging. Of course, the code is quite simple and could be refactored to use whatever else you need. HttpClient is nice because it's included in the Android SDK.

The code is up on GitHub here:

https://github.com/codeka/appengine-channel-api-java

Usage is quite simple, I don't have a sample available at the moment, but basically:

URI appEngineURI = new URI("https://myapp.appspot.com/");
// URI appEngineURI = new URI("http://localhost:8080/");

// Get a Channel token from the server
String token = "<TOKEN HERE>";

ChannelClient cc = new ChannelClient(appEngineURI, token,
  new ChannelListener() {
    @Override
    public void onOpen() {
      System.out.println("* onOpen()");
    }

    @Override
    public void onMessage(String message) {
      System.out.println("> "+message);
    }

    @Override
    public void onError(int code,
         String description) {
      System.out.println("* onError("+code+
         ", \""+description+"\")");
    }

    @Override
    public void onClose() {
      System.out.println("* onClose()");
    }
  });

Now, after going through all of this trouble, I'm not entirely sure I'll continue using this code. Don't get me wrong, it works perfectly well (though it's not been load-tested or anything and obviously the error handling leaves quite a bit to be desired) but the main issue that I'm not entirely sure if there's a huge benefit here over simple polling. It might require less network traffic during quiet periods, but I would guess that during heavy periods, where there's a lot of chatting going on, the network savings won't be that great. And polling is a heck of a lot simpler.

But for now, it's there and I'll use it.

Managing Email

Posted by

I get upwards of 200 emails a day at work. However, with the system of filters that I've set up, maybe 6-10 actually hit my inbox. The rest go to labels where I can read them (or not) at my leasure. A lot of people seem to find email overwhelming, but with the system I have, I can read important emails as soon as they come in, and leave the rest for a few times a day where I just go through my labels and "Mark as read" after scanning them for interesting tidbits.

We use Gmail at work (actually, Google Apps) so this is pretty specific to setting up Gmail filters, but the general idea is the same for any email client.

So the vast majority of email I get is because of mailing lists I'm subscribed to, rather than email that's addressed directly to me. It's the email that's addressed directly to me that's actually important, so for the most part, I want all mailing list emails to go to some label other than my inbox, and only things addressed directly to me come to my inbox. The easiest way to get started is after you've subscribed to a mailing list, wait for that first message to arrive. Then, from the drop-down on the menu, choose "Filter messages like this":

This will pop up the filter/search popup (the new Gmail interface isn't really as good as the old one in this respect, in my opinion) into which you can type additional criteria. The one I usually choose is to add "to:me@example.com" (i.e. my email address) in the "Doesn't have" field (for reasons that will become clear in a second):

Next, click "Create filter with this search". This will take you to the next step where you set up the actions you want to take. Because it's a mailing list and I don't want it to appear in my inbox, I check "Skip the Inbox", then I check "Apply the label:" and give it a nice label name. Finally, I'll check the "Also apply filter to..." checkbox.

Clicking "Create filter" then starts the whole show.

Now, once I've done this for all of the mailing lists I subscribe to, what I do a couple of times per day, I'll see my email looks like this:

(Sorry, it's my work email, I've blurred some of the "sensitive" label names...)

As you can see, I've got a couple of unread emails in a few of the labels, so I'll just go through them, scan the subject lines. If they look interesting, I'll read it. If not, I'll just choose "Mark all as read" from the "More" drop-down to mark all the messages as having been read. Now obviously, when I come in of a morning, I'll have quite a few more than this small selection of unread emails (I'm in Australia and the majority of my workmates are in the U.S.) so a sweep of my email takes a little longer first thing in the morning. But generally, I spend no more than about 15 minutes in the morning, and 5 minutes or so a couple of times per day go through this routine.

A couple of important points about my system:

  • The "to:me@example.com" in the "Doesn't have" part of the filter is important. The reason being, if I reply to a mailing list message, generally I'll want to be sure to read any replies to my replies. Having that in the filter ensures that any messages which are sent directly to me in addition to being sent to the mailing list will appear in my inbox. This is particularly important with things like the "escalations" label -- if someone adds me directly to the "To" field in an escalation, it's something I need to read!
  • You need to be a little OCD about marking messages as read. If you don't, then you end up ignoring the messages in labels altogether, and that really does defeat the purpose.
  • I typically don't have one-label-per-mailing list. Usually, I have many mailing lists going into one label (for example, my "misc" label has the "misc-au" mailing list, "menu", "forsale" and all those occasionally-interesting-but-not-right-now mailing lists). Keep the number of labels small enough that you can fit the whole list on your screen at once (the "right-side chat" lab is good for adding a bit of extra vertical real estate to the label list). This way, you can see at a glance which labels have unread messages in them.
  • Another trick for keeping the label list short is to choose the "Show if unread" option from the little drop-down menu to the right of the label. That way, if the label has no unread messages, it'll be hidden from the list (click on "More" to see it).
  • I keep my inbox empty. If a message doesn't need to be actioned then it's archived. Generally, there's only 3-4 messages that I actually keep around (to remind me of whatever). Theoretically, you could star messages you want to remember and just leave the rest in your inbox, but the sense of accomplishment in clearing your inbox -- actually clearing it -- is really worth the additional burden of having to search a bit more for older email.

My recommendation, if you're one of the ones feeling overwhelmed and you want to try my system, is to start from scratch. Remove all of your filters, all of your labels, and archive all of your email as of this moment. Trust me, you'll feel really liberated! Then as emails start coming in, set up your filters and labels as I've described above. You can create new labels directly from the filter setup window, so that's pretty easy. You can also retroactively apply labels to messages you've already archived, so all of your past email will appear in the correct labels, too.

Changing the icon of an application launcher in Unity

Posted by

You'd think this would be easy, right? You've just upgraded to Ubuntu 11.10 and it's got the fancy new "Launcher" for launching application. Now, it's not without it's problems, but here's one I wasn't expecting. One of my applications (SQLYog under Wine in this case) didn't have an icon. All I got was this:

Missing Icon

No big deal, right? In all previous version of Gnome (and Ubuntu) you could right-click on an icon, choose properties and select an icon. Guess what?

Right-click Menu

Not very useful, is it? So how do you do it? There's apparently not GUI at all for editing the icon of a launcher, and this is apparently the second version of Ubuntu to feature the launcher -- I might have accepted that there was no way to do it in version 1.0, but surely this isn't so uncommon that you need to be editing obscure files in hidden directories still, is it?

Apparently it is, because that's exactly what you need to do. So, which file does that launcher correspond to? I have no idea, because I couldn't figure out where that was configured. What I did find, though, was that in a subdirectory under ~/.local/share/applications there was a file called SQLyog.desktop (you will also find a bunch of .desktop files under /usr/local/applications). Opening up the SQLyog.desktop revealed the following:

[Desktop Entry]
Name=SQLyog
Exec=env WINEPREFIX="/home/dean/.wine" wine C:\\\\windows\\\\command\\\\start.exe /Unix /home/dean/.wine/dosdevices/c:/users/Public/Start\\ Menu/Programs/SQLyog\\ Community/SQLyog.lnk
Type=Application
StartupNotify=true
Path=/home/dean/.wine/dosdevices/c:/Program Files/SQLyog Community
Icon=/usr/local/share/icons/hicolor/sqlyogcommunity.png

The Icon entry pointed to a file that did not exist (I'm pretty sure it existed in a previous installation of Ubuntu, but maybe the upgrade to 11.10 deleted it?). In any case, I edited that file to point to a file that did exist, save it, and... nothing. OK, maybe you need to log out and back in? Nope. Still the "missing" icon.

So perhaps the launcher is using some .desktop file that I couldn't find for this?

$ find / -name "*yog.desktop" 2>/dev/nul

Nothing doing. It only found the one that I was editing. So I gave up, pressing the Start key, I entered "main menu" which opens the main menu editor from the pre-Unity days, and I can see the menu entry for SQLyog in there has indeed picked up the new icon I gave it. So I tried something daring: what if I drag'n'dropped the menu item across?

Drag'n'Drop

What the heck is that? Clicking it, it just opens up my browser to the default page. How does dragging a program icon from the "main menu" editor result in a link that opens my browser? No idea.

Anyway, I eventually solved the problem by opening up the Start key menu, searching for "SQLyog", which came back with the correct icon (I don't know how... maybe it looks up the applications menu as well?). Don't launch SQLyog from there, because you end up with some "Wine Windows Program Loader" in the launcher instead. Instead, just drag the icon from the Start key menu onto the launcher.

But now the problem is, when I click the SQLyog icon, I still get that "Wine Windows Program Loader" icon in the launcher while the program is running. I guess even version 2.0 of Unity is still "rough around the edges".

The problem is, this only encourages people to turn off Unity which means the problems never get solved.

Why you need to keep regular backups of your Subversion repository

Posted by

Sometimes we assume that the software we use is infallible. It works most of the time, so we just think that it'll always work.

Unfortunately, that's not always the case. Even if the software is 100% perfect, there are still things that can cause us problems. Disk drives are physical machines, and as with anything physical they wear out over time. Eventually, they break. And when that happens, you may find yourself in the same situation I was in yesterday: a broken Subversion repository

I did an svnadmin verify and it would return errors all over the place (in a different place each time, too). Eventually, I had to concede that the repository was un-recoverable.

Luckily for me, I do weekly backups and my backup from last week was still usable. My backup strategy is actually very simple: hot-backup.py every sunday night. Technically, I probably don't have to use hot-backup.py, since it's unlikely that I'm going to be doing anything at 4am on a Sunday morning, but you never know :)

So I bought a new hard drive and kicked off the restore process. I don't have a huge repository (~900 revisions [and counting!] and about 900MB.) - mostly the size comes from the fact that I've got copies of a few open source projects in there... I don't know why; I should probably tidy up my repository one of these days...

Anyway, the restore process is really simple: just bunzip the file that hot-backup.py created last Sunday. It took a few minutes (a bit longer actually, cause I took the opportunity to upgrade to Subversion 1.6.11 while I was at it).

The most annoying thing was that after it was all complete, my repository was complete only up until last Sunday: I had to take my current working directory and manually apply all the changes that I had made since then to get it back to what it was. Isn't there a corollary to Murphey's Law that states that when the worst thing that could happen happens, it happens at the worst possible time? In my case, it was because my drive failed on a Sunday: just hours before the next backup would've occurred and I had a full 7 days worth of changes to manually reapply.

So it's always a good thing when a hardware failure shows that all your obsessive backing up is actually worthwhile, but even with the best plans in place, there's always a lesson to learn. In my case, I've decided to augment my weekly backups with some daily incremental backups. I've borrowed this script and set it up so that it backs up incrementally each night as well.

I think the next step would be to do some off-site backups. I've just got my backups happening to a different drive in the same computer (I was lucky, I guess, that only one drive failed). I really need to get my backups saved somewhere a little more isolated. But maybe I'll wait until the next failure precipitates it...