This article does not pursue any practical goals - I just wondered how about 15 years ago, developers managed to make fully functional applications and games for weak phones of that time.


ITKarma picture


  • If anything, I have nothing to do with this game.

For example, the game from the picture above did not use floating-point numbers, since not all phones supported them. 3D and physics are completely self-written on fixed-point computing on top of integers. But it seems to me that listing the features of one application will not be very informative. For the sake of completeness, I’ll touch on the capabilities of phones, the j2me platform and at the same time compare this with modern Android development.

In addition, j2me is a full-fledged java of the old version (it seems 1.3), I added some missing classes and was able to run the.jar file with the game on my PC. The screenshot above is from there. I will not say that there is any benefit from this - just the API for j2me was very simple and I wanted to try it.


Phones of the time.


In technical terms, modern phones have gone very far, but in terms of functionality, in my opinion, even old phones allowed you to do all kinds of interesting things.


My first phone was Nokia 5200 , and instead of an “abstract smartphone in a vacuum” I’m better I will describe its features:


  • appeared in 2006
  • display 128x160 pixels in size as much as 1.8 inches
  • The display supported as many as 262 thousand colors. (if I understand, these are 6 bits per R, G, B components).
  • Infrared port and bluetooth for transferring files to other phones.
  • several megabytes of long-term internal memory (I don’t remember exactly, they write about 7 MB on the Internet)
  • slot for microSd card, I seem to have had 256 MB.
  • even then there were some primitive browsers and opera mini (which, incidentally, also literally weighed a hundred kilobytes)
  • 640x480 pixel camera.
  • MIDP 2.0 support ( wiki )
  • the phone was able to pretend to be a flash drive when connected via usb
  • but charged through some kind of connector
  • I did not find what kind of performance the processors had, but it was obviously very modest - no more than 100 MHz frequency and, possibly, a very slow or missing implementation of floating-point numbers. Unfortunately, the phone died a long time ago and I can’t run any benchmarks on it.

I will not list the mobile games that I played on it - I’ll just say that they all weighed 50-200kb and at the same time had a fairly deep gameplay.


Write once, run everywhere


Initially, java was positioned specifically for any low-power household devices. And she very well registered on the phones of that time, allowing you to run the same game on the phones of various manufacturers. Of course, there were platform-specific features, but theoretically the code should work the same everywhere.


The language was simple, bytecode too. It contains byte-long commands for a virtual stacked machine - writing a naive interpreter is not so difficult. And for this reason, the bytecode took up little space. Interestingly, inside the.jar, j2me applications contain the most real.class files from full java. The only difference was that the mobile application used the Canvas, MIDlet, and other classes from the javax.microedition package to interact with the outside world.


In my opinion, it is brilliantly simple. Compared to this, Android seems to be a set of crutches: the code is compiled into.class files, then converted to.dex (or several, since one dex file does not support more than 65k methods), is packed into apk, and then the mobile device recompiles it under ART .


In addition, mobile java supported multithreading. As we will see later, when writing applications, you could not do without it. Interestingly, the view on multithreading was a bit different then.For example, in the standard Vector class, all methods were synchronized. Now Vector is considered obsolete, and it is proposed to use the usual ArrayList in single-threaded code and the same ArrayList in multi-threaded, but explicitly capture the lock on it.


Another interesting feature is that there were no generics in the old java. The type system consisted of primitives (int, boolean,.) And objects. The same Vector class stored objects inside itself and returned them as Object, and the programmer then casted the resulting one to the desired type.


By the way, when I see go and manual castes to the necessary types, I recall java 1.3. Only go was stuck in development for some reason, and in the case of java, version 1.5 was released in 2004 with support for generics. But 1.3 was still used in mobile development.


Since the mobile j2me application is the most common.jar with normal.class files inside (albeit in the 1.3 format, which is already 20 years old), you can use this and run Gravity defied directly on the PC. There are no classes in javax.microedition in the "big" java, but you can write them yourself. The task is even simpler, since there are classes with almost the same set of methods: for example, CDMY0CDMY and CDMY1CDMY.


J2ME application architecture


ITKarma picture


To interact with the outside world, classes from CDMY2CDMY were used. Perhaps due to the limited capabilities of the phones, or maybe just because of the sense of beauty of the developers, the set of classes is very small, and they themselves are as simple as possible. For an example, look at the classes in javax.microedition.lcdui .


To create the application, you had to inherit from the class MIDlet .


When the application starts, it calls the CDMY3CDMY method.
If the application was minimized, CDMY4CDMY was called and CDMY5CDMY again when it was reopened. When fully closed, CDMY6CDMY was called.


In addition, the application itself could call the CDMY7CDMY and CDMY8CDMY methods to indicate that it was paused or terminated.
In addition, the application might be asked to “exit the pause” using the CDMY9CDMY method


Honestly, I do not really understand the meaning of CDMY10CDMY, since without it the applications worked fine, no one killed them.


It seems to me that this should be the lifecycle application of a healthy person.


In practice, it usually turned out that the application class inherited from Runnable and the implementation looked something like this:


Thread thread; boolean isRunning; boolean needToDestroy=false; public void startApp() { isRunning=true; if (thread != null) { thread=new Thread(this); thread.start(); } } public void pauseApp() { isRunning=false; } public void destroyApp(boolean unconditional) { needToDestroy=true; } 

And that's it, then the application lived in its own flow.


void run(){ while(!needToDestroy) {//игровой цикл здесь//и если на паузе - спим. while (!isRunning) { Thread.sleep(100); } } notifyDestroyed(); } 

It seems to me that waking up a stream every 100 milliseconds is not scary, and such an approach could well exist on modern phones. In addition, no one forbids stopping the stream after calling CDMY11CDMY and restarting it in CDMY12CDMY


Canvas


Another class was used for drawing on the screen - Canvas . It looks like a desktop Canvas in java.


You can call the CDMY13CDMY method in any thread, which will hint the system to update the image. After that, in the UI thread, the system will call CDMY14CDMY. Perhaps someone will have Vietnamese flashbacks again, but nothing happened when minimizing the application with Canvas - the object remained valid throughout the life of the program. The only difference is that in the background, the CDMY15CDMY calls were ignored and the CDMY16CDMY method was not called.


What is noteworthy, already then supported touch displays: there were methods
CDMY17CDMY, which on the touch phone returned true. When pressed, methods of the CDMY18CDMY type (moving the pointer), as well as the versions for CDMY19CDMY (start of pressing) and CDMY20CDMY (pressing completed) were called.
There is no multitouch support - well, okay, then there were no suitable displays.


Fonts and menus


Which is funny, then there were already three font sizes - small, medium and large. The specific dimensions depended on the phone. But, given the size of the displays, it looks normal. I don’t think that even a 240x360 display needs a lot of font sizes. Most importantly, bold and italic fonts are supported.


Now it is somehow unified, but earlier the back button on smartphones could be located both on the left and on the right. There was some kind of menu creation mechanism in j2me, so that the system itself drew menu items, supported scrolling, etc., and the application simply recognized the number of the selected item. For example, on the nokia 5800, such menus could be rewound with a finger, even if the application developers had no idea about it.


MIDlet Pascal


Once I was in school, I mastered Pascal and did not know other languages. From scratch, I couldn’t get into java2me development, but luckily, I learned about the existence of MIDlet pascal and I wrote my first applications on the phone on it. Later on, I switched to j2me in a rather funny way - I made an application on pascal midlet, decompiled, and looked what happened in java.


What's inside?


Well, enough nostalgia, we’ll better see how Gravity Defied is made and how it fits in 64 kilobytes.


First of all,.jar is a zip archive whose contents weigh 122.1 kB


  • In the META-INF folder there is 3.8 kB MANIFEST.MF, which lists the game files and SHA-1 and MD5 hashes for them. As well as the name of the main class and the file with the application icon.
  • 5.1 kB levels.mrg file in a rather compact form contains information about all 30 game levels. An average of 170 bytes per game level. I’ll tell you more about such a compact storage format later.
  • 11 pictures. In total, about 10.8 kB. Some of them are atlases with a bunch of sprites
  • .class files, totaling 98.3 kB, 15 pieces. The smallest is 127 bytes, the largest is 24.3 kB. Their size can be divided into groups:
    • two interfaces, (4 methods each), 127 and 174 bytes.
    • a simple class with six fields and a pair of methods - 470 bytes.
    • 9 classes of reasonable size - from 1.6 to 6 kB.
    • god-like classes:
      • m (I later renamed it to MenuManager ) - 24.3kB, it contains all the possible menus and messages of the game. No layout.xml and strings.txt :). The game did not imply support for multiple languages.
      • i ( GameCanvas ) 15.2 kB: a lot of code related to drawing, but, oddly enough, not all.
      • b ( GamePhysics ) 20.6 kB: calculation of game physics and for some reason a lot of code related to drawing.

Decompilation


We need a decompiler. I took fernflower - this is the one that is built into intelliJ IDEA. It seems to work fine - the output is code that is quite realistic to compile back. A few years ago I tried other decompilers and they could not cope.


A repository with IDEA weighs more than a gigabyte and clone it for a long time - instead, you can use the mirror in which decompiler only.


The decompiler assembly is trivial: CDMY21CDMY, the desired.jar will appear in the build folder
Decompiling a little harder: CDMY22CDMY


By default, the CDMY23CDMY option is not enabled, and without it you get code in which variables and methods can have funny names like CDMY24CDMY or CDMY25CDMY. The bytecode does not prohibit such names, but the java compiler will not like this. With CDMY26CDMY, the decompiler will convert the fields to a type of CDMY27CDMY or CDMY28CDMY - it will not get any worse, but the code will become valid.


The game was released back in 2004 - and, notably, obfuscation was already in use:


  • Bytecode in some form stores the names of classes, methods and fields. And the names of one or two letters allowed us to reduce the size of.class files.
  • There are surprises in the form of invalid names like CDMY29CDMY, CDMY30CDMY, etc.
  • It was possible to cut out unused methods and again make the code more compact. In the game, only the Micro class was not subjected to “renaming”, and only in it I found unused methods.
  • maybe for fixed-point calculations methods that were inline were used. I doubt that the developers manually wrote code like CDMY31CDMY
  • I don’t know, several classes were manually glued into one or with the help of an obfuscator, but in the game you can find such a Schrodinger class.

Putting it back


Looking at the code is good, but not informative. I want to run it and, possibly, add debug output or something else. I also want to restore the normal names of variables, methods and classes.


Many, many years ago I wrote code on Netbeans, and to build j2me applications I had to download a special SDK. The build seems to have used ant. For more information, please see here.


But the old Nokia didn’t work anymore, and I wanted to build and run the code directly on my computer and preferably on Linux. So I prepared a gradle project and tried to compile the code. The code was not compiled - there were not enough classes from CDMY32CDMY. Logically - they are not in the PC version of java. I decided to go in for bicycle building and simply mechanically added all non-existent classes and methods. The game uses a small subset of the available methods and classes, so it took no more than an hour.


For convenience, I brought CDMY33CDMY to the terminal and looked at the list of errors in real time. After adding each new method, CDMY34CDMY saved the changes.


The implemented classes can be viewed here .
There are only 19 of them:


  • 8 for saves in javax.microedition.rms.
  • 10 in.lcdui, which are responsible for images, fonts, Canvas, etc.).
  • MIDlet is the main class from which the application should inherit.

After the project compiles without errors, you can try to bring it to a beautiful view.


The application has a Micro class in which method names are not obfuscated. In general, it is logical that methods like startApp are inherited from MIDlet and it is impossible to rename them.


Sample code:


protected void pauseApp() { c=true; if (!b) { this.gameToMenu(); } System.gc(); } 

It is quite possible to guess that the variable CDMY35CDMY could be called CDMY36CDMY.


For "simple" methods, you can quite often understand what is happening:


public void a() { if (this.recordStore != null) { try { this.e.closeRecordStore(); return; } catch (RecordStoreException var1) { } } } 

and rename it something like CDMY37CDMY.


In addition, in intelliJ IDEA you can see all the calls of some methods. Especially - those for which we wrote stubs, such as CDMY38CDMY:


this.p=Image.createImage("/splash.png"); 

Obviously, p can be renamed to splashImage.


From some point of view, this is like solving Sudoku - you find obvious points, give meaningful names to variables and methods. This simplifies the understanding of the rest of the methods, giving them names... Huge respect to the Jetbrains developers - I climbed the code for at least ten hours, renaming variables with methods - and the code never broke. However, I still compiled the code from time to time and made sure that it remained working.


At one point, I got tired of it. I tried to run the code and it crashed - because all my stubs like loadImage ()... returned null and did nothing. It's time to write an implementation for stubs.


Most of them were done trivially: for example, the Image class:


public class Image { public final java.awt.Image image; private Image(java.at.Image image) { this.image=image; } public Graphics getGraphics() { throw new RuntimeException(); } public int getWidth() { return image.getWidth(null); } public static Image createImage(int w, int h) { return new Image(new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)); } ... } 

Of course, the API does not match in some places, but it is still very similar.


For drawing, I made the class CanvasImpl, which inherits from JPanel. He handled button presses and draw calls and turned them into calls to our game Canvas.
I even got a taste and did the upscaling of the picture so that I didn’t have to peer into the tiny window:


public void paintComponent(Graphics g) { if (upscale == 1) { canvas.paint(new javax.microedition.lcdui.Graphics(g)); } else { canvas.paint(new javax.microedition.lcdui.Graphics(screen.getGraphics())); g.drawImage(screen, 0, 0, width * upscale, height * upscale, Color.WHITE, null); } } 

CDMY39CDMY is my wrapper over awt.Graphics, which translates drawing calls.


Over and over again, I ran the code, watched it crash in different places and appended implementations for my stubs. The game swallowed some exceptions, but then it didn’t work correctly.


For example, if the game doesn’t work with pictures, the game works, draws the motorcyclist with simple lines, wheels with sticks. But it slows down on the PC. And why? For some reason, in the game, the background is masked with CDMY40CDMY pictures, and if the picture is not loaded, an empty picture of CDMY41CDMY pixel size is substituted, and when the display is tiled, it will be drawn CDMY42CDMY once or something. Apparently, the game initially worked without pictures with graphics from sticks and circles, then the developers added pictures, but did not test how it works without them.


In addition, there are fun bugs with saving. Perhaps I myself incorrectly implemented it, but the bottom line is that the game at some points tries to write in an invalid place, then it catches an exception, swallows it and quietly works on.


Honestly, I didn’t google what ready-made j2me emulators for PC are. Most likely there is - I used some kind of year in 2010 when I just mastered programming and tried to write games. If in existing emulators there are not enough features and you want to modify mine to support some more games - write in a personal.


Earned!


In the end, I managed to compile the decompiled code along with my stubs, run and play.


After that, I tried to connect the original.jar with the game to the stubs. There were several errors. It turns out that it is important not only that the method is called as it should, but that it also has the correct parent class. Because of this, I had to inherit Alert and Canvas from Displayable and place the abstract methods that I originally declared in Canvas. Well, alright, there weren’t so many fixes.


Now in the gradle project this structure is:


  • The emulator module with the code, in fact, of my proprietary emulator. Many methods are missing, I implemented only the minimum that was necessary for the game to work.
  • The app-original module, which contains the original.jar in the dependencies and allows it to be launched
  • The app-from-sources module with decompiled and reduced to a more or less decent appearance sources. It also starts.

We bring the code to a beautiful view


After the decompiled code can be compiled and run, understanding it has become easier. You can add debug output or turn off individual methods to understand what they are doing.


I think now it’s possible to discuss in more detail the features of the implementation.


Fixed-point physics


Normal integers are used for calculations. They are stored in the form of ordinary ints and it is believed that the lower 16 bits are a fractional part. Thus, we get numbers that take values ​​from CDMY43CDMY to CDMY44CDMY in increments of CDMY45CDMY.


This solution seemed very beautiful to me - decent fixed accuracy combined with a very large range of values ​​used.


Adding and subtracting such numbers is no different from similar operations with int.


Multiplication: if we simply multiply int, then we get the fractional part, and the whole "overflows". For multiplication, the numbers were first converted to long. When multiplying, we get a number of 64 bits with a fractional part of 32 bits. After a 16-bit right shift, you can go back to fractional 16 bits and trim the number back to int.


Division: with simple division, the fractional part is lost. Instead, you again need to convert to long, shift the dividend by 16 bits to the left and divide. For some reason, the game was done differently, a shift to the left by 32 and after - to the right by 16.


The game has implementation for CDMY46CDMY, CDMY47CDMY and CDMY48CDMY.


Made quite simple - there is a hard-coded array of 64 values ​​- an angle from 0 to 90 degrees. When calculating the sine or cosine, the angle is reduced to this range, then the index is calculated - and the value from the array is read from it. The result is an accuracy of one and a half degrees. Apparently, this is enough for the game.


Touch input


ITKarma picture


What is funny, this game in its two thousand and fourth year supported him. If the CDMY49CDMY method returns CDMY50CDMY, then an additional circle is drawn in the game for control, into which you can poke. The mode is made more likely for a tick - all the same, the entered values ​​are quantized to “full throttle”, “full tilt”, “full brake” and you can’t do anything “halfway”. But be that as it may, there is support for touch input in the game.


Level format


I mentioned above that the average size of a game level is 170 bytes. How did it happen? Very simple - just a couple of bytes are used to store each point.


Rather, it’s done a little trickier - first, all information is stored, such as start/finish position, etc., as well as the number of points. For storing points made two modes. If the first byte is 0xff, then an int pair with absolute coordinates comes next as coordinates, but if the byte is different, this byte is the offset dx relative to the previous point, followed by the byte with the offset dy.


for (int i=1; i < pointsCount; ++i) { byte modeOrDx; if ((modeOrDx=var1.readByte()) == -1) { offsetY=0; offsetX=0; pointX=var1.readInt(); pointY=var1.readInt(); } else { pointX=modeOrDx; pointY=var1.readByte(); } offsetX += pointX; offsetY += pointY; this.addPointSimple(offsetX, offsetY); } 

Entire code


Simple and effective.


Texture atlases


Unlike modern phones with GPUs and full hd screens, older telephones had very modest displays of the type (CDMY51CDMY or CDMY52CDMY). The need to twist images and draw 3D objects somehow did not arise, and in the api for pictures this is not even possible. The only thing that was possible when drawing the picture was to rotate it 90-180-270 degrees and flip it.


It seems to me that for that time it was not a problem - a sprite with a size of a dozen pixels has not many visible rotation options.
Specifically, in this game, 32 or 16 sprites were used for the motorcycle body, body parts and helmet of the motorcyclist. Helmet sprites required a picture as large as 48 * 48 pixels and weighing 1091 bytes.


However, it should be noted that at that time there was already some very primitive API for 3D graphics with a fixed 3D pipeline and I even played something 3D. The textures were with huge distinguishable pixels, and the octagonal wheels of the cars were perceived as something normal and highly detailed.


It seems to me - the size of the displays and the capabilities of the phones are very well suited for sprite 2D graphics and did not pull 3D.


Caching strings


During the game, time is drawn in the lower right corner of the screen. What is funny - the developers decided not to create each frame in a new line and made a lazily populated cache for 100 lines of the form" 23 "and" 64 ".


if (time10MsToStringCache[time10MsPart] == null) { String zeroPadding; if (time10MsPart >= 10) { zeroPadding=""; } else { zeroPadding="0"; } time10MsToStringCache[time10MsPart]=zeroPadding + time10Ms % 100L; } 

Honestly, I don’t know if it made sense to bother so for the sake of two digits. It might be easier to draw the numbers one at a time.


No-MVP architecture


Class MenuManager as a hardcode contains all the menus (they are all created once when the class is initialized) and, if necessary, draws them. If some component of the game needs to know what the current level is now, it just goes to the menu object with a choice of level and asks which position is active.


Perhaps someone will say that you need to separate the model from its presentation and apply all sorts of patterns for abstraction. But on the other hand: see how a similar task can be done in Android:


  • There is a layout file with a button layout. Separate for each menu.
  • Lines such as button names are moved to a separate stings.txt file.
  • There is an Activity that is recreated for each sneeze.
  • There is LifecycleObserver, which makes it possible to bind the text field to the Adapter so that when Activity dies, it can die and not occupy memory.
  • Adapter converts data from a DataSource.
  • It’s just that passing classes between threads is not ok, so we’ll still pack them in the Bundle and unpack them after receiving from the DataSource.
  • In DataSouce, data is thrown from different streams or even synchronized with a SQLite database.

Question: is this all definitely needed to handle the user's movement through the in-game menu? I may be wrong in the details, but the essence of the problem should be obvious.


Several classes are glued into one


I wondered for a long time why the class TimerOrMotoPartOrMenuElem and called it so for a reason.
I don’t know if the obfuscator did this or did it manually, but this class is simultaneously used to represent at least three different entities:


  • pieces of a motorcycle with coordinates and so on
  • of a menu item with some text
  • timer, which can be set to "in a second" or something like that

Accordingly, if the class is used as a menu item, one field in the class is used, if others are used as a motorcycle element.


Most likely, this was done in order to reduce the size of the application and fit it into 64 kilobytes. Apparently, the individual classes weighed significantly more than the “assembled” into one.


Long loading


I found that a game that builds from scratch is faster than in a second, and it takes a long time to load. “Hmm, suspicious,” I thought, and climbed to look for the source of the brakes.


What did I find :


public void init() { long timeToLoading=3000L; Thread.yield(); this.gameCanvas=new GameCanvas(this); Display.getDisplay(this).setCurrent(this.gameCanvas); this.gameCanvas.requestRepaint(1); while (!this.gameCanvas.isShown()) { this.goLoadingStep(); } long deltaTimeMs; while (timeToLoading > 0L) { deltaTimeMs=this.goLoadingStep(); timeToLoading -= deltaTimeMs; } this.gameCanvas.requestRepaint(2); for (timeToLoading=3000L; timeToLoading > 0L; timeToLoading -= deltaTimeMs) { deltaTimeMs=this.goLoadingStep(); } while (gameLoadingStateStage < 10) { this.goLoadingStep(); } this.gameCanvas.requestRepaint(0); this.isInited=true; } 

The game calls up the screen and then calls CDMY53CDMY over and over for three seconds. Then it calls up the screen update again, switching the picture to another and again calls CDMY54CDMY for three seconds. And after that calls CDMY55CDMY until they are completed.


CDMY56CDMY itself made pretty funny too :


private long goLoadingStep() { ++gameLoadingStateStage; this.gameCanvas.repaint(); long startTimeMillis=System.currentTimeMillis(); switch (gameLoadingStateStage) { case 1: this.levelLoader=new LevelLoader(); break; case 2: this.gamePhysics=new GamePhysics(this.levelLoader); this.gameCanvas.init(this.gamePhysics); break; case 3: this.menuManager=new MenuManager(this); this.menuManager.initPart(1); break; ....///аналогичный код для 5-8 case 9: this.menuManager.initPart(7); break; case 10: this.gameCanvas.setMenuManager(this.menuManager); this.gameCanvas.setViewPosition(-50, 150); this.setMode(1); break; default: --gameLoadingStateStage; try { Thread.sleep(100L); } catch (InterruptedException var3) { } } return System.currentTimeMillis() - startTimeMillis; } 

And in MenuManager.initPart () also contains a huge CDMY57CDMY block with loading steps.


Honestly, I don’t know why it was done that way. It would be possible just to show the logo in a separate stream and then after three seconds switch the picture to another, and in the main stream calmly load everything you need.


Creation History


The original game originally appeared in 2004. She was written for the Excitera Mobile Awards 2004 (EMA04) and won the best-in-show. There were three Swedish developers at Codebrew Software:


  • Tors Björn Henrik Johansson - system/game logic/interface, testing, levels design
  • Set Elis Norman - graphics/physics/mathematics/system/tools programming, levels design
  • Per David Jacobsson - physics programming, game graphics, levels design

In addition, there is a port of this game for android , made by our compatriots, but it just uses a decompiled code.


Since there have been no complaints about that code for five years, I think that my attempt to decompile the original application and understand it will not do any harm. Now the game is of historical interest and allows you to take a closer look at the era of small smartphones that could.

.

Source