hex007 / freej2me

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

Game screen "slides to the right" #61

Closed Nokia64 closed 4 years ago

Nokia64 commented 4 years ago

I've experienced this behavior in: Road Warrior [Gameskitchen] (128x128): rwarrior-2 Title screen and game playfield show the same exact problem. The screen output appears to slide out of the screen, interestingly only on some parts of the game. Also, please note, this game exposes another common bug in FreeJ2ME, the flickery menus. I do believe the menu is meant to be always drawn on top of game canvas (every frame?), not only when created.

Halley Wars [Taito] (128x128) hwars-2 Title screen appears to slide out of the screen. You can't see anything further. Halley Wars menu on a real phone (Nokia 3100), for reference: hwars

I don't really know if they are caused by the same bug in both games. Given their similar appearance I've decided to put both them on the same issue.

Thank you!

recompileorg commented 4 years ago

The good news is that Road Warrior isn't obfuscated. The decompiled source is a welcome break from the usual fare. Of course, it hasn't yet been terrible helpful.

The splash doesn't strangely scroll to the right, but the title image does. The paint method that draws both switches based on a mode variable, helpfully described by a few constants. frustratingly, both are drawn the exact same way!

public static final int MODE_GAMEOVER = 5;
public static final int MODE_ENTERNAME = 4;
public static final int MODE_TOPSCORES = -2;
public static final int MODE_TITLE = 0;
public static final int MODE_SPLASH = -5;
public static final int MODE_WON = 6;
public static final int MODE_BRIEFING = 2;
public static final int MODE_GAME = 1;
public static final int MODE_ABOUT = 11;

[...]

if (this.mode == 0 && !b)
{
    graphics.drawImage(this.title, (RWGame.width >> 1) - (this.title.getWidth() >> 1), (RWGame.height >> 1) - (this.title.getHeight() >> 1), 0);
    return;
}

[...]

if (this.mode == 0 && !b)
{
    graphics.drawImage(this.title, (RWGame.width >> 1) - (this.title.getWidth() >> 1), (RWGame.height >> 1) - (this.title.getHeight() >> 1), 0);
    return;
}

(The >> 1 divides by two. The intent is to draw the image centered on the display. )

My best guess is that there's something wrong with how I handle translation, or some difference between older and newer Nokia phones. I'll keep at it, but be on the lookout for a game that breaks after whatever fixes these two games.

Menu's are strange. Most are fine, but I wonder if I need to call the paint method for some types of displayables and not others. (Calling 'paint' on all of them breaks a lot of games.) Something to think about.

Nokia64 commented 4 years ago

Tried both games on a newer Nokia phone. They appear to work the same way they do on my good old Nokia 3100.

I'd like to point out Microemulator can run the Road Warrior game: https://code.google.com/archive/p/microemu/downloads To run it, you have to manually include the nokiaui libraries: java -cp "./lib/microemu-nokiaui.jar:microemulator.jar:Road_Warrior.jar" org.microemu.app.Main com.ape.j2me.roadwarrior.RoadWarrior Don't get me wrong, I'm not encouraging you to "copy-paste" Microemulator into FreeJ2ME. I don't even know if Microemulator's license(s) are compatible with FreeJ2ME ones, but I think it's (partial) NokiaUI implementation could be a great reference for FreeJ2ME development.

The "flickery" menus issue is actually pretty common on older J2ME games (2002-2005). I'd collect the best examples I can and open an issue showcasing them when I get some time to do testing on real phones. I'd also like to test on non-Nokia phones too (I have S.E. and Sagem)

recompileorg commented 4 years ago

Thanks for that.

Both games are perfectly playable when I disable translation. (Halley Wars needs slowed to 60fps and the phone mode set to Nokia to work. You can set those, which are saved per-game, from the options menu.)

There's clearly something wrong with translate, which makes me wonder if i should just leave it disabled. I could use an example of a game that uses it, but only works with it enabled.

recompileorg commented 4 years ago

Okay, there's a huge difference between Java2's Graphics2D and J2ME's Graphics translate I overlooked. It works now. The menu problem still exists.

recompileorg commented 4 years ago

Okay, both games work properly and the menu issue is fixed.

recompileorg commented 4 years ago

Well, fixing translate breaks a lot of newer games.

I added a configuration option that should sort things out. Config really needs a cleanup...

Nokia64 commented 4 years ago

I've collected the following SDK docs from many different Nokia phones:

Standard Nokia phones: Nokia 3410 SDK docs.zip Nokia 3410 (B&W 96x65 screen, released year 2002) Nokia 3510i SDK docs.zip Nokia 3510i (96x65 screen, released year 2002) Nokia 7210 SDK docs.zip Nokia 7210 (128x128 screen, Nokia Series 40 v1, year 2002) Nokia S40 6th Edition SDK docs.zip Series 40 platform v6 (year 2009) Symbian based Nokia phones (completely different J2ME implementation, less compatible with standard Nokia one): Nokia S60 2nd Edition FP3 MIDP SDK docs.7z.zip Nokia N97 SDK docs.zip Released 2009 Other phones: Siemens-SDK-docs.zip i-Mode SDK reference.zip Japanase I-Mode phones SDK documentation.

I'm posting them here at this issue since seems like GitHub does not offers any place to make discussions without opening new issues at people's projects.

Also, I'd like to point out, the game Road Warrior is now playable when set to Nokia(old), but the "Dialog screen" at start of game is now misplaced (displaced to the left), unlike in real hardware. I don't believe there's such a fragmentation between older/newer Nokia APIs.

Thank you!

recompileorg commented 4 years ago

Thanks! That's going to be a very valuable resource going forward.

It's good to know about the dialog screen. I'll look at it soonish (I'm working on m3g now).

I do disagree about the difference between newer and older nokia phones. Road Warrior very clearly expects calls to translate not to sum. As far as I can see, without the option to change the behavior of translate, those two games will simply never work. The code isn't obfuscated, so you can check this as well. Who knows, maybe there's something I missed.

There are real differences between phones. if not in the api then in their implementations. This is why rotations are always broken. I first got them working against the game Bounce, but that broke rotations in other games. There doesn't seem to be a perfect solution to getting these games working short of endless configuration options -- and more people actually working on the code as tracking down bugs in specific games takes a lot of time.

Nokia64 commented 4 years ago

I've been testing on real phones. Added a new game to the mix, Tetris Mania: FreeJ2ME/Nokia: Road warrior: Completely broken. Dialog screen works fine through Tetris Mania: TM-FJ2ME-Nokia FreeJ2ME/NokiaOld: Road Warrior: RW-FJ2ME-NokiaOld RW-FJ2ME-NokiaOld1 Tetris Mania: TM-FJ2ME-NokiaOld Real phone (Nokia 3100, year 2003): Road Warrior: rwarrior-3100- Tetris Mania: tetrismania-3100- Real phone (Nokia 2710 fold, year 2009) Road Warrior: Rwarrior-Nokia2720f-

As you can see, Both a Nokia(Old) setting dependent game and Nokia setting dependent game are working on the same exact device.

I've been looking at the game's code. translate is only called on RWGame.class (checked all other classes) Nokia docs for Nokia 7210, a very similar model to the 3100 from the same era, state the following:

public void translate(int x, int y) Translates the origin of the graphics context to the point (x, y) in the current coordinate system. All coordinates used in subsequent rendering operations on this graphics context will be relative to this new origin. The effect of calls to translate() are cumulative. For example, calling translate(1, 2) and then translate(3, 4) results in a translation of (4, 6).

The application can set an absolute origin (ax, ay) using the following technique:

g.translate(ax - g.getTranslateX(), ay - g.getTranslateY())

So I'm assuming the Nokia setting behavior as the proper one.

translate gets called three times: Line ~571: graphics.translate(RWGame.xoff, RWGame.yoff); executed unconditionally on every paint loop. xoff and yoff seem to be the screen offset for the image to be centered. On a 128x128 screen phone the xoff value is 16 Line ~630: graphics.translate(-graphics.getTranslateX(), -graphics.getTranslateY()); executed only on mode == 2 (dialog screen). This seems like a reset of the translation back to absolute 0. So the game may not be assuming absolute behavior of translate at all. (Note: this is the only call to getTranslateX/Y on the whole game) Line ~1012: graphics.translate(-RWGame.xoff, RWGame.yoff); executed unconditionally inside a try{}. This resets X offset back to 0

I added some debugging output on FreeJ2ME's side (I'd like to do so on game's side, so I could try it out on Nokia's SDK). I logged translate and getTranslate calls:

While on logo screen:

Translate: X:16 Y:0 DestX:16 DestY: 0
Translate: X:16 Y:0 DestX:32 DestY: 0
Translate: X:16 Y:0 DestX:48 DestY: 0
Translate: X:16 Y:0 DestX:64 DestY: 0
Translate: X:16 Y:0 DestX:80 DestY: 0
Translate: X:16 Y:0 DestX:96 DestY: 0
Translate: X:16 Y:0 DestX:96 DestY: 0

it starts incrementing translation like mad... Line ~1012 appears not to be executed. When the dialog screen pops up:

Translate: X:16 Y:0 DestX:96 DestY: 0
GetTranslateX: 96
GetTranslateY: 0
Translate: X:-96 Y:0 DestX:0 DestY: 0
Translate: X:16 Y:0 DestX:16 DestY: 0
GetTranslateX: 16
GetTranslateY: 0
Translate: X:-16 Y:0 DestX:0 DestY: 0
Translate: X:16 Y:0 DestX:16 DestY: 0
GetTranslateX: 16
GetTranslateY: 0
Translate: X:-16 Y:0 DestX:0 DestY: 0
Translate: X:16 Y:0 DestX:16 DestY: 0
GetTranslateX: 16
GetTranslateY: 0

Line ~630 appears to get executed, I suppose since mode == 2. It reverses line ~571 translation Then while the car deploying animation, it goes again:

Translate: X:16 Y:0 DestX:16 DestY: 0
Translate: X:16 Y:0 DestX:32 DestY: 0
Translate: X:16 Y:0 DestX:48 DestY: 0
Translate: X:16 Y:0 DestX:64 DestY: 0
Translate: X:16 Y:0 DestX:80 DestY: 0

Then ingame (when score gets drawn and you can start controlling the car), line ~1012 appears to start getting executed:

Translate: X:16 Y:0 DestX:1856 DestY: 0
Translate: X:16 Y:0 DestX:1872 DestY: 0
Translate: X:16 Y:0 DestX:1888 DestY: 0
Translate: X:16 Y:0 DestX:1904 DestY: 0
Translate: X:16 Y:0 DestX:1920 DestY: 0
Translate: X:-16 Y:0 DestX:1904 DestY: 0
Translate: X:16 Y:0 DestX:1920 DestY: 0
Translate: X:-16 Y:0 DestX:1904 DestY: 0
Translate: X:16 Y:0 DestX:1920 DestY: 0
Translate: X:-16 Y:0 DestX:1904 DestY: 0
Translate: X:16 Y:0 DestX:1920 DestY: 0

It keeps the screen's position stable, but by the time it does It would require you a 4K TV to play the game the screen is already "screwed up"

I don't quite know what many of the Java's structures there mean so I can't get a deep understanding on what's going on the code. So, I neither get what the game really wants to be done. BTW, I'm a complete clueless Java noob but I'd say this code is a huge mess. Might it be related to why line ~1012 does not gets executed?

I've also noticed the following being called: graphics.setClip(0, 0, RWGame.swidth, RWGame.sheight); Nokia docs state the following:

public void setClip(int x, int y, int width, int height) Sets the current clip to the rectangle specified by the given coordinates. Rendering operations have no effect outside of the clipping area.

Are those coordinates meant to be affected by the transform offset ones? Could this be affecting somehow?

Also, I've noticed that starting the game in Nokia(Old) mode and switching it to Nokia on title screen makes the game playable somehow.

I will attempt to add logging to the game and check out what do they spit out on Nokia's SDK emulator. Time to fire up a Windows VM :worried: . Will report back here what I find out.

EDIT: as usual I screwed up something I only notice when hitting the "publish" button

recompileorg commented 4 years ago

Well, I can't argue with real hardware. The question, I suppose, is what is "resetting" the transform?

Nokia64 commented 4 years ago

I managed to add logging to the game. It didn't rebuild entirely but the paint loop, RWGame.class was somehow compiled. Nokia's SDK Emulator didn't like it through, it gave me an "Error verifying class", so I performed the tests on KEmulator. It's reporting the same exact log output than it did when logged on FreeJ2ME's side, but showing correct graphical output.

At the moment I've verified that the public void paint(final Graphics graphics) function into RWGame exits with nonzero translate values and when it runs again it's initial values are zero. Such paint function is called from a loop in RWGameCanvas:

    public void paint(final Graphics graphics) {
        synchronized (this) {
            if (this.myGame.ready) {
                this.myGame.paint(graphics);
                this.myGame.ready = false;
            }
        }
    }

I don't know what the graphics thing being passed to it is. I couldn't track it down.

as a function it is, shouldn't it values be reset every time it gets called?

Nokia64 commented 4 years ago

Small clarification: Placed a "Hook" at paint() function's beginning that prints the getTranslateX() / getTranslateY() output. On KEmulator, every time it begins the values are X:0 Y:0, on FreeJ2ME it starts with the values of the previous run.

recompileorg commented 4 years ago

The graphics object is the graphics context the paint method will use to draw. It also holds the current clip region and transform, among other things.

This is passed by an instance of Canvas (myGameCanvas [RoadWarrior.java line ~19] the games instance of RWGameCanvas which extends FullCanvas which extends Canvas) in the run method [RWGameCanvas.java line ~181] which calls repaint. The canvas repaint method is [implementation dependent](https://docs.oracle.com/javame/config/cldc/ref-impl/midp2.0/jsr118/javax/microedition/lcdui/Canvas.html#repaint()) and may call paint. My implementation calls paint, passing the instance of PlatformGraphics associated with the PlatformImage used to hold the canvas's image.

Exhausing, isn't it? Adele Goldberg famously said "In Smalltalk, everything happens somewhere else". I think of this just about ever time I work in Java.

You may be right about needing a clean graphics instance. From the documentation

Operations on this graphics object after the paint() call returns are undefined. Thus, the application must not cache this Graphics object for later use or use by another thread. It must only be used within the scope of this method.

This may explain all of the odd problems. I'll give it a try.

recompileorg commented 4 years ago

Well, that did it. Tetris Mania now works, all the things reported wrong with RoadWarrior are fixed, and Halley Wars works correctly.

I'll remove Nokia (old) as an option and see how this affects rotations...

Nokia64 commented 4 years ago

Great! :heavy_check_mark: As of now I can confirm this fixes graphical garbage issues in: Ancient Empires 2 (splash screen effects) Rage of Mages +10 games of the Bobby Carrot series Choc'o'Bloc (Title screen) Namco's Rolling With Katamari Calling Yuki Sumea's Fantasy Warrior 2 Evil / good versions Gameloft's Rayman Bowling Gameloft's Rainbow Six 3 Morpheme's Balloon Headed Boy Ghost Blade FIFA 2004 Will keep an eye out for game breakages. Thank you!