APIs Are Fun

Prelude

As I’m sure you’re well aware of by now if you have read any of this blog, I really like working on 2009Scape and hacking on the RuneScape client.

If you aren’t aware (which, given the size of the community that’s interested in this sort of thing, you’re probably not) the RuneScape client is actually very closed source.

People like me who wish to preserve the game and learn how it works have to break it apart, crack it open, and dispell the ancient hieroglyphics imposed by the obfuscator to try to get a feel of what’s going on.

Below: Ancient Hieroglyphics

			for (local115 = 0; local115 < 3; local115++) {
				local126 = (local115 * 256 + 128) / 3;
				local130 = 256 - local126;
				@Pc(207) byte local207 = (byte) (arg12 * local126 + arg0 * local130 >> 8);
				@Pc(252) short local252 = (short) (((arg6 & 0x7F) * local130 + (arg4 & 0x7F) * local126 & 0x7F00) + (local130 * (arg6 & 0x380) + local126 * (arg4 & 0x380) & 0x38000) + (local126 * (arg4 & 0xFC00) + (arg6 & 0xFC00) * local130 & 0xFC0000) >> 8);
				for (local162 = 0; local162 < local41; local162++) {
					if (local115 == 0) {
						local103.addTriangle(local109, local113[0][(local162 + 1) % local41], local113[0][local162], local252, local207);
					} else {
						local103.addTriangle(local113[local115 - 1][local162], local113[local115 - 1][(local162 + 1) % local41], local113[local115][(local162 + 1) % local41], local252, local207);
						local103.addTriangle(local113[local115 - 1][local162], local113[local115][(local162 + 1) % local41], local113[local115][local162], local252, local207);
					}
				}
			}

This leads to us creating some really questionable code sometimes, particularly when we want to modify the client to add some cool new feature.

Which led me to the idea I had yesterday evening and implemented over the course of the last 24 hours.

The Idea

The idea was simple: Do not do the dumb thing. Do not directly modify the freshly-decompiled and half-renamed code to do cool new things. Abstract it and modularize it.

I wanted to create an API and a plugin system. And I wanted to do it without leaning on any external libraries.

Design/Layout

Obviously, if you want a plugin system and an API, you’ve gotta come up with a desgin for it. The API bit was simple: class full of static methods that abstract engine functionality in an easily accessible way.

The hard part there is just finding the bits of client functionality to tie into your API.

As for the plugins themselves: also simple. The client I did this with was already set up to read configuration data from a JSON file, so I just had that point to a plugins directory the client should load plugins from.

Plugins themselves are structured in a flat file manner: Each plugin has its own folder, and inside that plugin folder it should have a single class file named plugin.class, and a single .properties file named plugin.properties, for storing author attribution, version, etc.

plugins/
 -> MyAwesomePlugin
    -> plugin.class
    -> plugin.properties

The Plugin class needs to be some pre-compiled class that extends a base Plugin class I made, which contains callbacks to various bit of engine code, such as Draw(deltaTime), Init(), OnLogout(), and so on.

I got all this put together, spun up a very basic HelloWorld plugin that would just print Hello world to the console when it had its Init() method called, and much to my surprise, it worked! I would be lying if I said I did not get very, very, very excited at this point.

Early Problems

So one of the very first things I needed to solve before I got too ahead of myself was the thing I mentioned earlier: getting an API together. This might seem pretty easy, you know, “Just make some functions that relay the sane input to the less sane engine functions.”

I wish it was that easy. You see, the hardest part with the whole thing is just tracking down where the engine code even is, because it’s all written in ancient hieroglyphics, and scattered around a million seemingly-unrelated files.

Drawing text at arbitrary coordinates on the screen was an easy one, my good friend Pazaz who kindly let me perform this experiment of mine on his WIP client, had already found and renamed most of the related engine functions there.

The biggest thing that gave me trouble early on was Jagex’s internal JagString data type. It’s their own that they came up with to replace instances of strings in the client for… some reason. My buddy Woahscam and I had already ripped these out of a different client we worked on together and it didn’t seem to cause any issues, so JagString seems to ultimately serve no real purpose.

You might be asking, “Ok, unique data type for strings, weird! But what’s the actual issue?”

The actual issue is that JagString has almost-zero support for using simple non-alphanumeric characters - or at least, relies on arbitrary key sequences like “(U” and “)1” to substitute for these things, causing garbled text like this if you’re naive enough to think you can use a comma in a string:

Garbled Text

This makes it really, really annyoing for end users to use any kind of API to draw text on the screen. Which is really not too great of a thing when you’re making the API for people to use in writing their own plugins.

So, I went on a scavenger hunt, or at least that’s a very positive way of phrasing it. It’s actually a lot more like plunging into madness and hoping patterns emerge from the void.

Which in my case, thankfully, a pattern did emerge from the void, staring back at me.

	@OriginalMember(owner = "client!sj", name = "a", descriptor = "(Ljava/lang/String;I)Lclient!na;")
	public static JagString method3952(@OriginalArg(0) String arg0) {
		@Pc(14) byte[] local14 = arg0.getBytes(StandardCharsets.ISO_8859_1);
		@Pc(23) JagString local23 = new JagString();
		local23.chars = local14;
		local23.length = 0;
		for (@Pc(31) int local31 = 0; local31 < local14.length; local31++) {
			if (local14[local31] != 0) {
				local14[local23.length++] = local14[local31];
			}
		}
		return local23;
  }

Notice anything peculiar? No? Let’s rename a bit of this and get a better look.

	@OriginalMember(owner = "client!sj", name = "a", descriptor = "(Ljava/lang/String;I)Lclient!na;")
	public static JagString of(@OriginalArg(0) String string) {
		@Pc(14) byte[] bytes = string.getBytes(StandardCharsets.ISO_8859_1);
		@Pc(23) JagString js = new JagString();
		js.chars = bytes;
		js.length = 0;
		for (@Pc(31) int i = 0; i < bytes.length; i++) {
			if (bytes[i] != 0) {
				bytes[js.length++] = bytes[i];
			}
		}
		return js;
	}

Ah! Much better. So, what this method does, is it iterates through every byte in an ISO-standard normal String, and directly creates a JagString with those bytes! And sure enough, displaying some text with non-alphanumeric characters by first turning the normal string into a JagString using this specific method, displays all the text correctly.

Really just makes you wonder what even the point of JagStrings really was. Probably obfuscation.

Getting Some Results

Eventually I had managed to stick my fingers in enough bits of the engine code to get a half-decent API cobbled together. I implemented things like the overhead debug rendering you can see in my post about RuneScape Bitfields and some other cute little development/debug utilities.

Then I got brave. Like, really brave.

I decided I was going to use the API and the plugin system to make something that was actually fun. Bizarre, I know.

So I dived back into the abyss and rummaged around until I managed to scrounge up some methods for preparing the Software/GL rasterizers for drawing as well as actually drawing rects to them (thankfully again, my buddy Pazaz had already found and rename these methods.)

The resulting API methods were a bit like this:

    public static void ClipRect(int x, int y, int width, int height) {
        if (IsHD()) {
            GlRaster.setClip(x,y,width,height);
        } else {
            SoftwareRaster.setClip(x,y,width,height);
        }
    }

    public static void FillRect(int x, int y, int width, int height, int color, int alpha) {
        if (IsHD()) {
            if (alpha != 0)
                GlRaster.fillRectAlpha(x,y,width,height,color,alpha);
            else
                GlRaster.fillRect(x,y,width,height,color);
        } else {
            if (alpha != 0)
                SoftwareRaster.fillRectAlpha(x,y,width,height,color,alpha);
            else
                SoftwareRaster.fillRect(x,y,width,height,color);
        }
    }

I then needed some way to obtain and draw sprites. Again, thankfully Pazaz had already found and renamed these methods, so all I had to do was wrap them up neatly in the API:

    public static Sprite GetSprite(int spriteId) {
        Sprite rawSprite = null;

        if (client.js5Archive8.isFileReady(spriteId)) {
            rawSprite = SpriteLoader.loadSprites(spriteId, client.js5Archive8);
        }

        return rawSprite;
    }

And then with a little bit of finesse and careful use of sprite/GL draw calls, I managed to create a couple of pretty cool things:

XP Drops (something the 2009 client never had!):

My Simple XP Drops

And a cute little Slayer kills remaining tracker:

Simple Slayer Tracker

All as modular plugins using my own API and plugin system.

Overall, I was pretty satisfied with myself.

If you’re interested in taking a look at some of these plugins or the plugin-enabling engine code, they are available here on my fork of Pazaz’s client on Github. I’ve already spoken to Pazaz about it, and he wants this system included upstream, so soon you’ll be able to check it out on his upstream repository as well.