hex007 / freej2me

A free J2ME emulator with libretro, awt and sdl2 frontends.
Other
500 stars 78 forks source link

Image loading issue at Gamebo's Omega Mission #117

Closed Nokia64 closed 2 years ago

Nokia64 commented 2 years ago

The game Omega Mission by Gamebo (tested JAR md5: 9f353fbf1c7bf54aa306ce63ed7f1fd6) crashes during initialization in FreeJ2ME due to it passing a damaged PNG image to javax.microedition.lcdui.Image.createImage(byte[], int, int). The images load fine in KEmulator and a Nokia 7230

Tracing it back, such image is stored into a packed resources file (/1 into the JAR) and read into a byte array by a custom routine at the game (GameMidlet.a(String,int,int)) which uses a DataInputStream to read the file from the JAR then pass it to createImage(). The image data is 15KB but it appears to be truncated at around ~7KB as passed to createImage() shot A small visual treat to spice up the issue

The game expects a single call to DataInputStream.read(byte[] b, int off, int len) to return all the data and ignores its return value, while according to the javadocs it appears not to neccesarily do so (https://nikita36078.github.io/J2ME_Docs/docs/midp-2.0/java/io/InputStream.html#read(byte[],%20int,%20int)) (I'm unsure about it, though. Don't clearly understand when it's supposed to return less data than available and when to block)

I think the FreeJ2ME-passed InputStream object doesn't returns all the available data from the stream on a read(byte[] b, int off, int len) call, behaviour this game appears to rely on. The game code is obfuscated and also performs an intermediate transform on the data, though. Will try setting up a small test MIDlet instead to be able to better peek into what's going on.

Thank you!

recompileorg commented 2 years ago

That read method should always block waiting for data. If data can't be read, an IOExceptionshould be thrown. Data should be stored in b[] as it they're read until len bytes have been read or an IOException is thrown.

In the event of an IOException, any bytes that were already stored in b[] will still be stored. This should be the only case where less than len bytes of data are returned.

(While the documentation doesn't clarify, I expect that no bytes will be read and stored in b[] in the case of an IndexOutOfBoundsException as that can be detected early.)

Sorry that I can't be of more help at the moment...

woesss commented 2 years ago

I tried to launch this game and found a problem in RMS. The game catches the RecordStoreNotFoundException, but the RecordStore catches it on its own code and rethrows a more generalized RecordStoreException: https://github.com/hex007/freej2me/blob/cce8347047159a29f44f2c5a9ec0e9ac78609d51/src/javax/microedition/rms/RecordStore.java#L102-L113
As for images, KEmulator loads them using SWT, since ImageIO is more capricious - that is, the problem is probably in the features of the image format.

Nokia64 commented 2 years ago

Thanks a lot woesss! #118 's working nicely for me, too The RMS patch will fix a huge ton of games, btw What was ultimately the issue? I see you entirely reworked getResourceAsStream()

Also thank you recompileorg for clarifying on the read() method! I'll wait for the PR to be merged and close this issue

P.S: Now there's another mystery 😉 out The same exact problem appears at KEmulator too, but not a real Nokia I'll open a new issue for this later (hopefully soon, been real busy lately -.-)

recompileorg commented 2 years ago

Sorry it took so long. We've got a new baby and things are a bit crazy.

Thanks woesss for the great work!

woesss commented 2 years ago

@Nokia64, You guessed correctly - the problem is in the read method, it does not guarantee that all data will be read in one call. ByteArrayInputStream doesn't have this problem.
Characters disappear because the clip area is set incorrectly - I did not understand why, possibly due to a thread race condition.

P.S. Hmm... I did not see the root of the problem - the main class is loaded by another method and is not processed with ASM, which replaces getResourceAsStream(). That is, in other games, resources from the main class code may not be loaded at all.

woesss commented 2 years ago

P.S: Now there's another mystery wink out

I figured out the reason for this problem. The game calls NokiaUI's DirectUtils.getDirectGraphics() and uses the resulting object to the end. But the emulator creates a new Graphics object for every call to paint, and setting it (like setClip()) has no effect on that DirectGraphics object because it uses the Graphics instance passed to the getDirectGraphics() method.