Need help with Java .class decompilation > compilation


cube48

Member
Joined
Oct 6, 2008
Messages
288
Location
Bohemia
Website
www.48.cz
Is there somebody skilled in Java here? I'm in need of help with decompilation > edit > compilation of one particular .class in Legend Of Yore.


I've contacted the developer of the game, Kevin. He's friendly guy and happy about port of LoY to Pandora but he's currently quite busy with normal life and work and might not have time soon for amending the variables defining the screen resolution of Pandora. The goal is to have a fullscreen resolution of the game (not sure if it will allow us to run it in true fullscreen). So far we can run it in 480x320 window only.


So I'm asking here if somebody would be able to dive into the loy-latest.jar (grab the Java version of the game at the dowload page linked above) and change all height values, currently at 600, into 480 in this class:


org.newdawn.touchapi.applet.TouchFrame.class


Currently, in the game if you try to switch to fullscreen, the game forces the resolution to 800x600 but it fails as it is not available at Pandora. By changing the height to 480 it should theoreticaly run in fullscreen. There is also one or two dialogue strings with actual error message containing the resolution. These strings are not crucial but I guess they should be changed too for the consistency.


I'm totally lame when it comes to Java but I've managed to decompile the .class file in a stand-alone decompiler and view what has to be changed. But it seems and it's also what Kevin wrote me that it must be than compiled together as a whole (with the rest of the files ...?). And this is where I got stucked. I've tried Eclipse with JAD plugin (and exe) but that even didn't show me the right decompiled source although I followed the JAD integration instructions.


Could anybody give it a shot, please? In case of success, send me the updated .jar. I'll add it to a PND and put you in credits of it ;-)


Thanks in advance for any help or suggestions.
 
One approach you could try:

  • Rename the .jar to .zip
  • Extract the .class file
  • Use a hex editor to search for "02 58" (or "58 02", depending on endian). Hopefully there, won't be many occurances (if lucky, just the one)
  • Replace with "01 E0" (or "E0 01"), save the change, add it back to the zip, overwriting the old file
  • Rename back to .jar and try it out.


If it doesn't work, and there's more than once instance, just work through them. One of them should work (unless Java does some weird formatting when saving numbers)
 
Thanks pmprog, I even didn't think of hexediting it ... cool, gonna try it!


I was already feeling that loading Eclipse and JAD is like hunting flies with RPG :)
 
Java Bytecode, especially when compiled by the Eclipse compiler, can be read like an open book. just look at JD-GUI, with it you can browse the source of the bytecode as if you actually had it, Eclipse even leaves the original variable names in. Just unzip all the stuff and create a .java file of the to be modified class where it belongs and copy the source from the decompiler into it, do you modification and then simply compile the single file from the root directory of the JAR's extracted content so it will find all the .class files, then everything just needs to be put together into a new JAR.
 
Thanks to all for suggestions.


Unfortunatelly


- hexediting doesn't work - game fails to start up


- JBE doesn't allow me to read the source directly (at least my Java lame eyes can't read the prestructured tree of weird stuff :-D)


- Eclipse - I've managed to decompile and read the sources of all other classes but not the TouchFrame.class. I'm getting just following error, no specific complaints.



Code:
/*jadclipse*/

//Error!


I'll continue cracking it. Of course any other suggestions are welcome. Thanks.
 
I've patched it, but I have no idea if it works (I refuse to install Java anywhere [Edit, except for my Android development VM])


Byte offset 0x0000244D [02 58 B5 00 27] -> replace with [01 E0 B5 00 27]


When I decompile after this patch, the section where "height" is set to 600 now says 480
 
Last edited by a moderator:
Thanks. Classeditor doesn't reveal the source code in it's pure form. If I search for "600" I get only the string of the error message.


Now I managed to decompile it with JD-GUI and can see the source in it's pure form.

Code:
package org.newdawn.touchapi.applet;

import edu.stanford.ejalbert.BrowserLauncher;

import java.awt.Color;

import java.awt.Container;

import java.awt.Dimension;

import java.awt.DisplayMode;

import java.awt.Graphics;

import java.awt.GraphicsDevice;

import java.awt.GraphicsEnvironment;

import java.awt.Image;

import java.awt.Rectangle;

import java.awt.Toolkit;

import java.awt.event.KeyEvent;

import java.awt.event.KeyListener;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.PrintStream;

import java.net.NetworkInterface;

import java.net.SocketException;

import java.net.URL;

import java.net.URLConnection;

import java.util.Enumeration;

import java.util.Properties;

import java.util.zip.GZIPInputStream;

import java.util.zip.GZIPOutputStream;

import javax.swing.JFrame;

import javax.swing.JOptionPane;

import org.newdawn.touchapi.Game;

import org.newdawn.touchapi.Sound;

public class TouchFrame

implements GameCanvasContainer

{

private static final int TIMEOUT = 3000;

private GameCanvas canvas;

private Properties save = new Properties();

private String className;

private int width;

private int height;

private boolean cheat;

private Properties props;

private boolean downloadingFailed = false;

private URLConnection connection;

private int total;

private String[] check = { "KEVGLASS.DSA", "KEVGLASS.SF", "MANIFEST.MF", "INDEX.LIST" };

private boolean fullscreen;

private DisplayMode originalMode;

private JFrame frame;

private String title;

public TouchFrame(Properties props, String title, String className, boolean big, boolean cheat)

{

this.title = title;

this.originalMode = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDisplayMode();

this.props = props;

this.cheat = cheat;

if (!cheat) {

	 for (int i = 0; i < this.check.length; i++) {

	 URL url = TouchFrame.class.getClassLoader().getResource("META-INF/" + this.check[i]);

	 if (url == null) {

		 System.exit(0);

	 }

	 if (!url.getProtocol().equals("jar")) {

		 System.exit(0);

	 }

	 }

}

this.className = className;

this.canvas = new GameCanvas(this, cheat);

this.width = 480;

this.height = 320;

if (big) {

	 this.width = 800;

	 this.height = 600;

}

if ("true".equals(props.getProperty("game.portrait"))) {

	 this.width = 320;

	 this.height = 480;

}

this.canvas.setMinimumSize(new Dimension(this.width, this.height));

this.canvas.setSize(new Dimension(this.width, this.height));

this.canvas.setMaximumSize(new Dimension(this.width, this.height));

this.canvas.setPreferredSize(new Dimension(this.width, this.height));

loadProperties();

createFrame();

}

private void createFrame() {

if (this.frame != null) {

	 this.frame.setVisible(false);

	 this.frame.dispose();

}

this.frame = new JFrame(this.title);

this.frame.setBackground(Color.black);

Image im = Toolkit.getDefaultToolkit().getImage("icon.png");

this.frame.setIconImage(im);

this.frame.getContentPane().add(this.canvas);

this.frame.setDefaultCloseOperation(0);

this.frame.addWindowListener(new TouchFrame.1(this));

}

private void initFrame()

{

this.frame.pack();

this.frame.setResizable(false);

this.frame.setLocationRelativeTo(null);

this.frame.setVisible(true);

this.canvas.createStrategy();

}

public void start() {

boolean enabledScreenShot = true;

initFrame();

this.canvas.start(this.className, new Rectangle(this.width, this.height), enabledScreenShot);

}

public Sound loadSound(String ref)

{

if (ref.endsWith(".ogg")) {

	 try {

	 return new OggSound(this.canvas.getResource(ref));

	 } catch (IOException e) {

	 throw new RuntimeException(e);

	 }

}

try

{

	 return new ApplicationSound(ref);

} catch (Exception e) {

	 throw new RuntimeException(e);

}

}

private void loadProperties() {

try {

	 FileInputStream fin = new FileInputStream(this.canvas.modded() ? "local.properties.dat" : "save.properties.dat");

	 int ver = fin.read();

	 fin.read();

	 fin.read();

	 GZIPInputStream zin = new GZIPInputStream(fin);

	 if (this.canvas.modded()) {

	 int value = zin.read();

	 if (value != 11) {

		 return;

	 }

	 }

	 this.save.load(zin);

	 zin.close();

	 fin.close();

	 if (ver == 1) {

	 String value = this.save.getProperty("__u");

	 if (!getUUID().equals(value)) {

		 this.save.setProperty("__a", "");

		 store();

	 }

	 }

} catch (Throwable e) {

	 System.err.println("Failed to load/find save games");

}

}

private void store() {

try {

	 FileOutputStream out = new FileOutputStream(this.canvas.modded() ? "local.properties.dat" : "save.properties.dat");

	 out.write(new byte[] { 1, 8, 1 });

	 GZIPOutputStream zout = new GZIPOutputStream(out);

	 if (this.canvas.modded()) {

	 zout.write(11);

	 }

	 this.save.setProperty("__u", getUUID());

	 this.save.store(zout, "");

	 zout.close();

	 out.close();

} catch (IOException e) {

	 System.err.println("Failed to save to: save.properties");

	 JOptionPane.showMessageDialog(this.frame, "Failed to save game!");

}

}

public void save(String name, String value)

{

if (value == null)

	 this.save.remove(name);

else {

	 this.save.setProperty(name, value);

}

store();

}

public String load(String name)

{

return this.save.getProperty(name);

}

public static void main(String[] argv) {

try {

	 Properties properties = new Properties();

	 properties.load(new FileInputStream("run.properties"));

	 String osname = System.getProperty("os.name");

	 boolean newWindows = osname.startsWith("Windows");

	 if ((osname.endsWith("95")) || (osname.endsWith("98")) || (osname.endsWith("XP"))) {

	 newWindows = false;

	 }

	 if ((osname.endsWith("NT")) || (osname.endsWith("2000")) || (osname.endsWith("ME"))) {

	 newWindows = false;

	 }

	 if ((!newWindows) &&

	 ("true".equals(properties.getProperty("use.opengl")))) {

	 System.setProperty("sun.java2d.opengl", "true");

	 }

	 TouchFrame frame = new TouchFrame(properties, properties.getProperty("game.title"), properties.getProperty("game.class"), "true".equals(properties.getProperty("game.big")), "true".equals(properties.getProperty("game.cheat")));

	 frame.start();

} catch (IOException e) {

	 System.err.println("Unable to locate run.properties");

}

}

public String getUUID()

{

try

{

	 Enumeration ifaces = NetworkInterface.getNetworkInterfaces();

	 while (ifaces.hasMoreElements()) {

	 NetworkInterface iface = (NetworkInterface)ifaces.nextElement();

	 if (!iface.isLoopback()) {

		 byte[] data = iface.getHardwareAddress();

		 if (data != null) {

		 String value = "";

		 for (int i = 0; i < data.length; i++) {

			 value = value + data[i];

		 }

		 return value;

		 }

	 }

	 }

} catch (SocketException e) {

	 e.printStackTrace();

	 return "" + new File(".").getTotalSpace();

}

return "" + new File(".").getTotalSpace();

}

public boolean keyPressed(KeyEvent event)

{

if ((this.cheat) &&

	 (event.getKeyCode() == 45)) {

	 String input = JOptionPane.showInputDialog(null);

	 this.canvas.getGame().inputCode(input);

	 return true;

}

String keyCode = "key." + event.getKeyChar();

String mapping = this.props.getProperty(keyCode);

if (mapping != null) {

	 if (mapping.equalsIgnoreCase("LEFT")) {

	 this.canvas.controlPressed(2);

	 return true;

	 }

	 if (mapping.equalsIgnoreCase("RIGHT")) {

	 this.canvas.controlPressed(3);

	 return true;

	 }

	 if (mapping.equalsIgnoreCase("UP")) {

	 this.canvas.controlPressed(4);

	 return true;

	 }

	 if (mapping.equalsIgnoreCase("DOWN")) {

	 this.canvas.controlPressed(5);

	 return true;

	 }

}

return false;

}

public void getRemoteResource(String u)

{

if (this.downloadingFailed) {

	 return;

}

try

{

	 URL url = new URL(u);

	 String[] segments = url.getPath().split("/");

	 String filename = segments[(segments.length - 1)];

	 File local = new File(filename);

	 if (local.exists()) {

	 return;

	 }

	 System.out.println("Download: " + url);

	 this.total = -1;

	 Thread t = new TouchFrame.2(this, url);

	 t.start();

	 long startTime = System.currentTimeMillis();

	 while (this.total == -1) {

	 try { Thread.sleep(100L); } catch (Exception e) {

	 }if (System.currentTimeMillis() - startTime > 3000L) {

		 break;

	 }

	 }

	 if (this.total == -1) {

	 this.downloadingFailed = true;

	 System.out.println("Download failed (timeout): " + u);

	 return;

	 }

	 byte[] buffer = new byte[100000];

	 InputStream in = this.connection.getInputStream();

	 FileOutputStream out = new FileOutputStream(local);

	 while (this.total > 0) {

	 int count = in.read(buffer, 0, buffer.length);

	 if (count < 0) {

		 in.close();

		 out.close();

		 local.delete();

		 this.downloadingFailed = true;

		 System.out.println("Download failed: " + u);

		 return;

	 }

	 out.write(buffer, 0, count);

	 this.total -= count;

	 }

	 out.flush();

	 in.close();

	 out.close();

} catch (IOException e) {

	 e.printStackTrace();

	 this.downloadingFailed = true;

	 System.out.println("Download failed: " + u);

	 return;

}

}

public InputStream getCachedResource(String ref)

{

try {

	 File file = new File(ref);

	 if (!file.exists()) {

	 return null;

	 }

	 return new FileInputStream(file);

} catch (IOException e) {

	 e.printStackTrace();

}return null;

}

public boolean resourceExists(String ref)

{

boolean existsInClassPath = TouchFrame.class.getClassLoader().getResourceAsStream(ref) != null;

boolean existsInLocal = new File(ref).exists();

return (existsInClassPath) || (existsInLocal);

}

public void setResolution(int w, int h)

{

this.width = w;

this.height = h;

this.canvas.setMinimumSize(new Dimension(this.width, this.height));

this.canvas.setSize(new Dimension(this.width, this.height));

this.canvas.setMaximumSize(new Dimension(this.width, this.height));

this.canvas.setPreferredSize(new Dimension(this.width, this.height));

this.frame.pack();

this.frame.setLocationRelativeTo(null);

}

public int getPlatform()

{

return 4;

}

public void show(String url)

{

try {

	 BrowserLauncher launcher = new BrowserLauncher();

	 launcher.openURLinBrowser(url);

} catch (Throwable e) {

	 e.printStackTrace();

	 try {

	 Process process = Runtime.getRuntime().exec("open " + url);

	 if (process.exitValue() != 0)

		 JOptionPane.showMessageDialog(this.frame, "Failed to launch browser\n\nNavigate to:" + url);

	 }

	 catch (IOException e1) {

	 e1.printStackTrace();

	 }

}

}

public boolean isFullScreen()

{

return this.fullscreen;

}

public void setFullScreen(boolean full)

{

GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();

if (full) {

	 try {

	 DisplayMode[] modes = device.getDisplayModes();

	 DisplayMode newMode = null;

	 for (int i = 0; i < modes.length; i++) {

		 DisplayMode current = modes[i];

		 if ((current.getWidth() == 800) && (current.getHeight() == 600)) {

		 newMode = current;

		 }

	 }

	 if (newMode == null) {

		 JOptionPane.showMessageDialog(this.frame, "No 800x600 screen mode available!");

		 return;

	 }

	 createFrame();

	 this.frame.setUndecorated(true);

	 this.canvas.setResolution(800, 600);

	 this.canvas.createStrategy();

	 device.setFullScreenWindow(this.frame);

	 device.setDisplayMode(newMode);

	 this.fullscreen = true;

	 } catch (Throwable e) {

	 e.printStackTrace();

	 setFullScreen(false);

	 JOptionPane.showMessageDialog(this.frame, "Failed to switch to fullscreen");

	 return;

	 }

} else {

	 device.setDisplayMode(this.originalMode);

	 device.setFullScreenWindow(null);

	 createFrame();

	 this.frame.setUndecorated(false);

	 this.canvas.setResolution(800, 600);

	 initFrame();

	 this.fullscreen = false;

	 return;

}

}

public Graphics getGraphics()

{

return this.frame.getGraphics();

}

public void addKeyListener(KeyListener keyListener)

{

this.frame.addKeyListener(keyListener);

}

public boolean isVisible()

{

return this.frame.isActive();

}

public boolean isActive()

{

return this.frame.isActive();

}

public void exit()

{

System.exit(0);

}

}

I've done this before but didn't know what to do with it and got some error on Win7 64bit machine. Now I'm fine on 32bits. Hmmm ... so what's next with the source code ... amend the values and inject it somehow into the project in Eclipse I guess. Any tips on how to replace/recompile the original class when I'm not able to decompile>edit it directly in Eclipse?


Edit: @pmprog, could you please send me the .class file? I'll PM you my e-mail. Thanks!
 
Last edited by a moderator:
Thanks for the file but unfortunatelly it doesn't work. Game fails to start.


Just FYI, game checks for the updates online and if it finds a newer version of loy-latest.jar it downloads it and overwrites any updated one.


If I disable the update in run.properties file of the game it just throws error "Failed to start game". It seems that hex editing doesn't help and complete recompilation is the way to go :-/


Edit: Thanks but even your second file didn't help.
 
Last edited by a moderator:
The JAR is protected via hashes, eventually it is enough to delete the META-INF/KEVGLASS files as well as all the file hash entries in the META-INF/MANIFEST.MF file (the first set of lines is mandatory).
 
The JAR is protected via hashes, eventually it is enough to delete the META-INF/KEVGLASS files as well as all the file hash entries in the META-INF/MANIFEST.MF file (the first set of lines is mandatory).
Interesting to know, thanks for the titbit!
 
Interesting, I've followed the suggestion of Letalis Sonus, used pmprogs class file and although the game doesn't run, it doesn't complain about failed start up. Looks like a slight progress :)
 
How about trying Letalis' change with all the original files? That way we can slowly eleminate where the problem lies
 
Removing KEVGLASS files and editing MANIFEST in combination with original loy-latest.jar doesn't throw error but game doesn't start.


I've also tried progressively hexediting .class file one value by one and always checking decompiled result. Somehow I'm not able to replace all 600 values without destroying the .class file. After replacing some of the 600 values the file becomes un-decompilable. No matter what, game doesn't start once I touch the .class file. Tried also all possible combinations with/without removing KEVGLASS files and MANIFEST edit.


The last hope now is the recompilation of the whole package. I should perhaps try to convince Kevin to compile extra version for Pandora.
 
I'm no Java expert, but have to you tried totally repacking the jar file, even with unaltered contents, and getting the game to run?


http://docs.oracle.com/javase/tutorial/deployment/jar/


Once you've done that, then you can look to replacing the class with the modded one I sent you. That link about talks about generating manifest files whilst packaging, so that should take care of validation checks.


Baby steps :p
 
So, I managed to to reinsert a changed class into the jar you asked for and it still starts. I can probably do any resolution you ask for, this is 800x480 for example:


c78fpz3vygix9dd7p.png



I don't own a Pandora so I can't try it there :)
 
Last edited by a moderator:
Looks great! I've send you a PM, nagelfar.


Now we have to see if Pandora's JRE accepts call for fullscreen with appropriate resolution fitting the screen.


pmprog, thanks for the link, that was exactly what I needed, some simple instructions about JAR basics.


P.S.: nagelfar, could you please describe us how did you manage to insert it into the JAR? Thanks!
 
Last edited by a moderator:
Technically, what I did was (and is) illegal so I'll contact the developers first and ask for permission to send you the manipulated jar. Sry if that sounds stupid for some of you but I'm a Programmer myself so I take stuff like that very serious. As a sidenote: I changed the program so you can configure any resolution you want so we can play around with it.


I'll ask for permission now and let you know about the result, bear with me :)


As for how I did it: Will also explain as soon as I get the "go ahead", you guys were almost there, there is only one additional thing you need to know ;)
 
I've already contacted the developer before I published the low-res PND. He even suggested the disassembly and recompilation of the class and he seemed to be happy about Pandora port. But I agree with contacting him again in case you touched the program further then just amending values in it. Thanks again for your effort. I really love this community.


Edit: You can refer to me when you are contacting Kevin but I guess he will connect it as I wrote him before that if I don't manage the update that I'll ask the community for a help.
 
Last edited by a moderator:
Back
Top