How To Create Your Own Bot Interface
1. What you need to know.
2. Setting Up A Project.
3. Creating a configuration.
4. Creating a boot loader.
5. Creating a Manifest.
6. Creating Utility functions.
7. Loading the client.
8. Creating the Frame/Implementing the Client.
9. Overriding the Canvas & Creating a PaintListener..
10. Drawing using our PaintListener/PaintObserver..
11. Attached Tutorial Files
What you need to know.
Arrays, Instances, References, Logic.
Various Programming Patterns: Subclassing, Observer Pattern, BootLoading, Multi-Casting/Chaining, Singleton.
Setting Up A Project.
For this tutorial, I will not be using command-line. I downloaded and chose a simple IDE with a simple and easy to use interface. This IDE is IntelliJ (Community Edition) which can be downloaded here: https://www.jetbrains.com/idea/download/
You can use whatever IDE you like but for the sake of this tutorial and boot loaders, I recommend IntelliJ.
Part One (Creating the project base):
- Click the file menu, hit New -> Project.
- Select Java from the left menu.
- Hit Next.
- Give your project a name (Ex: BotTutorial).
- Hit done.
The below images are the results of the above steps:
http://i.imgur.com/Bv2wDHa.png
http://i.imgur.com/CS3Vwv5.png
- Right-Click on the src folder. Hit New -> Package.
- Name your package (without quote): "eos".
- Right-Click on the src folder. Hit New -> Package.
- Name your package (without quotes): "java.awt".
- Right-Click on the eos package. Hit New -> Class.
- Name your class "Main".
- Add the below code to your class.
Java Code:
package eos;/**
* Created by Brandon on 2015-12-20.
*/public class Main
{ public static void main
(String[] args
) { }}
The below images are the results of the above steps:
http://i.imgur.com/0NKfx30.png
http://i.imgur.com/XCXtxTk.png
http://i.imgur.com/8HM7w0O.png
http://i.imgur.com/CxH2n0i.png
http://i.imgur.com/HiIO0Tj.png
http://i.imgur.com/ULjlLeZ.png
Creating a configuration.
A configuration is a script in Intelli-J that tells it which class is the main class (which class should be ran first).
- Click the button with the arrow pointing DOWN at the top right of your IDE. This is the configuration button.
- Hit Edit Configurations...
- Hit the plus sign at the top left corner of your IDE.
- Choose Application.
- Give your configuration a name (without quotes): "Main".
- Specify your main class (without quotes): "eos.Main".
- Specify the following VM Options (if you do not know where your Jar is located, look at your Working Directory on the same screen): -Xbootclasspath/p:"/Users/YourUserNameHere/Desktop/BotTutorial/out/artifacts/Eos/Eos.jar"
- Hit Apply.
- Hit Done.
The below images are the results of the above steps:
http://i.imgur.com/i0QhndW.png
http://i.imgur.com/dXY860r.png
http://i.imgur.com/FomWekk.png
Creating a boot loader.
For those of you that do not know what a boot loader is, it is a set of instructions/code that is ran before the actual application's main gets run. We need this because we want our bot to be double-clickable (double click the jar and it runs). We don't want to have to input command line arguments using cmd/terminal in order to run it every time.
- Right-Click the eos package. Hit New -> Class.
- Enter "BootLoader" as the class name.
- Paste the following code for the boot loader.
Java Code:
package eos;import java.io.IOException;import java.net.URLDecoder;/**
* Created by Brandon on 2015-12-20.
*/public class BootLoader
{ private static final String BOOT_FLAGS
= "-Xbootclasspath/p:"; public static void main
(String[] args
) throws IOException { String path
= BootLoader.
class.
getProtectionDomain().
getCodeSource().
getLocation().
getPath(); path
= URLDecoder.
decode(path,
"UTF-8").
replaceAll("\\\\",
"/"); StringBuilder commands
= new StringBuilder
(0xFF
); String mainName
= Main.
class.
getCanonicalName(); boolean windows
= System.
getProperty("os.name").
contains("Win"); if (windows
) { commands.
append("javaw"); } else { commands.
append("java"); } commands.
append(' ').
append(BOOT_FLAGS
); commands.
append('"').
append(path
).
append("\""); commands.
append(" -cp ").
append('"').
append(".").
append('"').
append(' ').
append(mainName
); for (String arg
: args
) { commands.
append(' ').
append('"').
append(arg
).
append('"'); } Process application
= null; Runtime runtime
= Runtime.
getRuntime(); if (windows
) { application
= runtime.
exec(commands.
toString()); } else { application
= runtime.
exec(new String[]{ "/bin/sh",
"-c", commands.
toString() }); } }}
The above code creates a JVM, runs your application with "XBootClassPath/p:" commands and arguments the original JVM dies off leaving the bot JVM running. This allows us to override internal classes without using command line and keeps our jar double-clickable.
Creating a Manifest.
A manifest is the part of the jar file configuration that tells the JVM what classes to run when the jar is double clicked or ran without explicit main arguments.
In our case, we want our boot loader to run first! Then the boot loader will run main on a separate JVM and then die off, leaving only the Main JVM running.
- Click the file menu -> Project Structure.
- Choose Artifacts from the left menu.
- Hit the plus sign and choose Jar -> From Modules With Dependencies.
- Specify the main class (without quotes): "eos.BootLoader".
- Hit OK.
- Put a checkmark in "Build On Make".
- Change the name of the Artifact to "Eos" (without quotes).
- Hit Apply.
- Hit OK.
The below images are a result of the above steps:
http://i.imgur.com/c05ooIJ.png
http://i.imgur.com/0a3gviu.png
http://i.imgur.com/1shmw3R.png
Creating Utility/Helper classes.
For this part of the tutorial, we will be creating a socket class and a Utilities class.
- Right-Click the eos package. Create a new package called "utility" (without quotes).
- Right-Click the utility package. Create a new class called "Util" (without quotes).
- Copy paste the below code into the Util class.
Java Code:
package eos.utility;import javax.imageio.ImageIO;import javax.swing.*;import java.awt.*;import java.net.URL;import java.text.SimpleDateFormat;import java.util.Calendar;import java.util.Date;import java.util.Random;/**
* Created by Brandon on 2015-12-20.
*/public class Util { private static final Random random
= new Random(); private static Util instance
= new Util(); private static final SimpleDateFormat dateFormat
= new SimpleDateFormat("MM-dd-yyyy-hh-mm-ss"); private Util() { } public static Util getInstance
() { return instance
; } public static int random
(int Min,
int Max
) { return random.
nextInt(Math.
abs(Max
- Min
)) + Min
; } public static void sleep
(int Time) { try { Thread.
sleep(Time); } catch (InterruptedException e
) { e.
printStackTrace(); } } public static void sleepRandom
(int Min,
int Max
) { sleep
(random
(Min, Max
)); } private static Date getDate
() { return Calendar.
getInstance().
getTime(); } public static Image loadResourceImage
(String ResourcePath
) { try { return ImageIO.
read(instance.
getClass().
getResource(ResourcePath
)); } catch (Exception e
) { e.
printStackTrace(); } return null; } public static ImageIcon loadIcon
(String url
) { try { return new ImageIcon(new URL(url
)); } catch(Exception e
) { e.
printStackTrace(); } return null; }}
The above class is our utilities class that does various things for us. It is a singleton class meaning that there is only ever ONE instance of the class. It cannot be allocated by the user of the class. The class allows us to load images and resource images, generate random numbers, sleep for a certain amount of time, and sleep for a random amount of time.
Next is our socket class for downloading the RS config file & whatever else we need to download (if any).
- Right click the utility package. Hit New -> Class.
- Name the class (without quotes): "HTTPSocket" and place the following code into the newly created class.
Java Code:
package eos.utility;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.net.HttpURLConnection;import java.net.URL;import java.net.URLConnection;/**
* Created by Brandon on 2015-12-20.
*/public class HTTPSocket
{ private URL url
; public HTTPSocket
(String Address
) throws IOException { url
= new URL(Address
); } private void setProperties
(URLConnection connection,
String userAgent
) { connection.
addRequestProperty("Protocol",
"HTTP/1.1"); connection.
addRequestProperty("Connection",
"keep-alive"); connection.
addRequestProperty("Keep-Alive",
"300"); if (userAgent
!= null) { connection.
addRequestProperty("User-Agent", userAgent
); } else { connection.
addRequestProperty("User-Agent",
"Mozilla/5.0 (" + System.
getProperty("os.name") + " " + System.
getProperty("os.version") + ") Java/" + System.
getProperty("java.version")); } } public String getPage
(String userAgent
) { return request
(null, userAgent,
false); } public String request
(String data,
String userAgent,
boolean post
) { try { HttpURLConnection connection
= (HttpURLConnection)url.
openConnection(); setProperties
(connection, userAgent
); connection.
setRequestMethod(post
? "POST" : "GET"); connection.
setUseCaches(false); connection.
setDoInput(true); if (post
) { connection.
addRequestProperty("Content-Length",
String.
valueOf(data.
length())); connection.
setDoOutput(true); try (OutputStreamWriter writer
= new OutputStreamWriter(connection.
getOutputStream())) { writer.
write(data
); writer.
flush(); } } String Line; StringBuilder Builder
= new StringBuilder
(); try (BufferedReader Reader = new BufferedReader(new InputStreamReader(connection.
getInputStream()))) { while ((Line = Reader.
readLine()) != null) { Builder.
append(Line).
append("\n"); } } connection.
disconnect(); return Builder.
toString(); } catch (Exception e
) { e.
printStackTrace(); return null; } }}
This class allows us to download web pages, config files, etc..
Loading the client.
The RS client can be loaded using 2 different techniques. The first technique is to load it like SMART or like the browser. This requires you to parse the HTML with a Regex. The second way to load it is like the Official RS client via the "jav_config.ws" file.
For the sake of this tutorial, I will be showing you how to load it like the Official RS Client. If you'd like to know how to load it like the browser does, you can PM me or check the source SMART's source code.
- Right-Click the eos package.
- Hit New -> Class.
- Name the class ClientApplet.
- Paste the below code into the file.
Java Code:
package eos;import eos.utility.HTTPSocket;import javax.swing.*;import java.applet.Applet;import java.applet.AppletContext;import java.applet.AppletStub;import java.awt.*;import java.net.URL;import java.net.URLClassLoader;import java.util.HashMap;/**
* Created by Brandon on 2015-12-20.
*/public class ClientApplet
extends JPanel implements AppletStub { private static final long serialVersionUID
= 5627929030422355843L
; private final HashMap
<String, String
> parameters
= new HashMap
<>(); private Applet applet
; private URLClassLoader loader
; private URL codebase
; private URL documentbase
; public ClientApplet
(String world,
int width,
int height
) { try { this.
setLayout(new BorderLayout(0,
0)); HTTPSocket socket
= new HTTPSocket
(world
+ "/jav_config.ws"); String lines
[] = socket.
getPage(null).
replaceAll("param=|msg=",
"").
split("\r|\n|\r\n"); //param=|msg=(.*?)\r|\n|\r\n for (String line
: lines
) { if (line.
length() > 0) { int idx
= line.
indexOf("="); parameters.
put(line.
substring(0, idx
), line.
substring(idx
+ 1)); } } codebase
= documentbase
= new URL(parameters.
get("codebase")); loader
= new URLClassLoader(new URL[]{new URL(codebase
+ parameters.
get("initial_jar"))}); applet
= (Applet)loader.
loadClass(parameters.
get("initial_class").
replace(".class",
"")).
newInstance(); applet.
setStub(this); applet.
setPreferredSize(new Dimension(width, height
)); this.
add(applet,
BorderLayout.
CENTER); } catch(Exception e
) { e.
printStackTrace(); JOptionPane.
showMessageDialog(null,
"Error Loading.. Please Check Your Internet Connection.",
"Error Loading..",
JOptionPane.
ERROR_MESSAGE); } } public void start
() { if (applet
!= null) { applet.
init(); applet.
start(); while(applet.
getComponentCount() < 1) { try { Thread.
sleep(10); } catch(Exception e
) { } } } } public void stop
() { if (applet
!= null) { applet.
stop(); applet.
destroy(); applet
= null; } } public Applet getApplet
() { return applet
; } public ClassLoader getClassLoader
() { return loader
; } @Override
public boolean isActive
() { return true; } @Override
public URL getDocumentBase
() { return documentbase
; } @Override
public URL getCodeBase
() { return codebase
; } @Override
public String getParameter
(String name
) { return parameters.
get(name
); } @Override
public AppletContext getAppletContext
() { return applet.
getAppletContext(); } @Override
public void appletResize
(int width,
int height
) { applet.
setSize(width, height
); }}
The above class actually loads the RS applet. It is capable of loading both OSRS and RS3. How does it work? When the Official RS client loads the applet, it gets the applet parameters from the following URL(s):
http://runescape.com/jav_config.ws
OR
http://oldschool.runescape.com/jav_config.ws
The result is a configuration file that looks like:
Code:
title=RuneScape
adverturl=http://www.runescape.com/bare_advert.ws
codebase=http://world1.runescape.com/
storebase=0
initial_jar=gamepackeRaq70Zx95k7978wYJvrdwQ7MorebrDI_6623136.jar
initial_class=Rs2Applet.class
viewerversion=124
win_sub_version=1
mac_sub_version=2
other_sub_version=2
browsercontrol_win_x86_jar=browsercontrol_0_-1928975093.jar
browsercontrol_win_amd64_jar=browsercontrol_1_1674545273.jar
termsurl=http://www.jagex.com/g=runescape/terms/terms.ws
privacyurl=http://www.jagex.com/g=runescape/privacy/privacy.ws
download=1513674
window_preferredwidth=1024
window_preferredheight=768
advert_height=96
applet_minwidth=765
applet_minheight=540
applet_maxwidth=3840
applet_maxheight=2160
msg=lang0=English
msg=lang1=Deutsch
msg=lang2=Français
msg=lang3=Português
msg=err_verify_bc64=Unable to verify browsercontrol64
msg=err_verify_bc=Unable to verify browsercontrol
msg=err_load_bc=Unable to load browsercontrol
msg=err_create_advertising=Unable to create advertising
msg=err_create_target=Unable to create target applet
msg=copy_paste_url=Please copy and paste the following URL into your web browser
msg=tandc=This game is copyright © 1999 - 2015 Jagex Ltd.\Use of this game is subject to our ["http://www.runescape.com/terms/terms.ws"Terms and Conditions] and ["http://www.runescape.com/privacy/privacy.ws"Privacy Policy].
msg=options=Options
msg=language=Language
msg=changes_on_restart=Your changes will take effect when you next start this program.
msg=loading_app_resources=Loading application resources
msg=loading_app=Loading application
msg=err_save_file=Error saving file
msg=err_downloading=Error downloading
msg=ok=OK
msg=cancel=Cancel
msg=message=Message
msg=information=Information
msg=err_get_file=Error getting file
msg=new_version=Update available! You can now launch the client directly from the RuneScape website, and chat timestamps work properly.\nGet the new version here: http://www.runescape.com/download
msg=new_version_linktext=Download Update
msg=new_version_link=http://www.runescape.com/download
param=13=true
param=35=false
param=29=443
param=21=-1007872315
param=20=
param=12=.runescape.com
param=52=443
param=23=lobby44.runescape.com
param=37=0
param=31=false
param=40=1
param=39=0
param=19=
param=3=0
param=6=33533
param=16=wwGlrZHF5gKN6D3mDdihco3oPeYN2KFybL9hUUFqOvk
param=45=eRaq70Zx95k7978wYJvrdwQ7MorebrDI
param=7=0
param=22=0
param=2=DEB8EF942367613913E5A4FD89E2CCFB4E6AFAFA94A0B5B049FBC3FAB530F4FF5EF63E4DAD5C45AA
param=43=
param=32=false
param=17=false
param=4=halign=true|valign=true|image=rs_logo.gif,0,-43|rotatingimage=rs3_loading_spinner.gif,0,47,9.6|progress=true,Verdana,13,0xFFFFFF,0,51
param=44=
param=28=43594
param=41=true
param=38=1757695916
param=46=443
param=33=1143
param=11=false
param=5=http://world1.runescape.com
param=48=0
param=1=80
param=24=false
param=18=43594
param=27=http://services.runescape.com/m=gamelogspecs/clientstats?data=
param=-1=t6xUGAxBP6eU3HBFKr2Dig
param=42=0
param=15=0
param=47=content.runescape.com
param=50=443
param=0=LdncX3BzNDKE5WuJeQ9alA
param=34=1200
param=36=false
param=25=38
param=8=1088940781
param=30=http://world1.runescape.com
param=51=0
param=10=43594
param=14=content.runescape.com
Now the file is separated by \r\n|\r|\n new line format. Each "param" is passed to the applet. Each "msg" is a localized string that can be used to display an error, an option, etc.. However, we don't care about msg's. We only care for params.
There are a few other parameters we need to load the applet and that would be:
Code:
codebase=http://world1.runescape.com/
initial_jar=gamepackg5QzAjWsDRm4MGtjZbXmyI7pl-psD5zq_798252.jar
initial_class=Rs2Applet.class
Our ClientApplet class loads the jar specified at the above codebase + initial_jar and loads the main class specified by initial_class.
This is the way the Official Client loads the applet.
Our start method starts the applet and waits until the Canvas is ready. Our stop method destroys the applet and frees all allocated resources.
Creating the Frame/Implementing the Client.
Now that we have every thing we need to load the client/applet. We need to be able to display it on screen. To do this, we need to create a window/frame and place the applet inside it.
- Go to your Main file.
- Copy the following code into the file.
Java Code:
package eos;import eos.utility.Util;import javax.swing.*;import java.awt.*;import java.awt.event.WindowAdapter;import java.awt.event.WindowEvent;/**
* Created by Brandon on 2015-12-20.
*/public class Main
{ private static JLabel splash
= new JLabel(Util.
loadIcon("http://www.runescape.com/img/game/splash.gif")); private static void displayFrame
(String world,
int width,
int height
) { JFrame frame
= new JFrame("Eos"); frame.
setDefaultCloseOperation(JFrame.
EXIT_ON_CLOSE); frame.
setResizable(false); if (splash
!= null) { frame.
add(splash
); frame.
pack(); frame.
setVisible(true); } centerFrame
(frame
); ClientApplet applet
= new ClientApplet
(world, width, height
); frame.
add(applet
); applet.
start(); if (splash
!= null) { frame.
remove(splash
); } frame.
revalidate(); frame.
pack(); centerFrame
(frame
); frame.
addWindowListener(new WindowAdapter() { @Override
public void windowClosing
(WindowEvent e
) { frame.
setVisible(false); applet.
stop(); frame.
dispose(); super.
windowClosed(e
); } }); } private static void centerFrame
(JFrame frame
) { Toolkit toolkit
= Toolkit.
getDefaultToolkit(); int cX
= (toolkit.
getScreenSize().
width / 2) - (frame.
getWidth() / 2); int cY
= (toolkit.
getScreenSize().
height / 2) - (frame.
getHeight() / 2); frame.
setMinimumSize(frame.
getSize()); frame.
setLocation(cX, cY
); frame.
setVisible(true); frame.
setResizable(true); } public static void notifyCanvasReady
(Canvas canvas
) { } public static void main
(String[] args
) { displayFrame
("http://runescape.com",
800,
600); }}
Now if we hit the green play button, we can see that we have successfully loaded the RS3 client. We can specify the OSRS url to load OSRS if we wanted. No difference in code
Now let's break it all down. The first function creates a JFrame (window). We add our splash screen/loading icon to the window and immediately display it. We centre the window on our screen because we're professional.
Next we load the applet (our splash screen will still be showing until the applet has started). We loaded the applet by creating a ClientApplet. We remove our splash screen and add the applet to the frame. We then re-centre the frame on our screen.
Finally, we add a window listener that destroys the applet when the user presses the "exit" button. This is because we need to manually start and stop the applet and once the applet has been stopped, we hide our frame and do cleanup. The JVM will then die right after and voila we are complete.
The client successfully loads and can be closed at any time!
Overriding the Canvas & Creating a PaintListener.
This is what we're all here for. We have successfully created a client but we want to be able to draw on it. We want to be able to tell it to do stuff. We want to listen to events! We have successfully created a boot loader that loads our custom canvas..
Oh but wait? We haven't created a canvas yet! Without a custom canvas, we cannot draw on the Applet because it is double buffered. This means that the client uses two buffers. One it draws on in the background and another it displays on screen. It swaps these two buffers constantly to avoid flickering on the screen. Thus if we draw on it, our drawing will disappear.
Anyway, let's override the java.awt.Canvas class with our own implementation. First we need to create a PaintListener class that allows us to draw on the Canvas that we will be creating.
- Right-Click the eos package.
- Hit New -> Package.
- Name it "listeners" (without quotes).
- Right-Click the listeners package.
- Hit New -> Class.
- Name the class: "PaintListener" (without quotes).
- Copy paste the following code into the newly created PaintListener file.
Java Code:
package eos.listeners;import java.awt.*;import java.util.EventListener;/**
* Created by Brandon on 2015-12-20.
*/public interface PaintListener
extends EventListener { public void onPaint
(Graphics g
);}
This class is an interface/observer/listener that will be added to our canvas. The canvas will call the onPaint(Graphics g) function to tell us to draw our custom drawings/art onto the client.
This is called the observer pattern. Why? Because we are observing whenever the Canvas draws.
Next we need to create our Canvas.
- Right-Click the java.awt package. (Please note this is NOT the eos package!).
- Hit New -> Class.
- Name your class "Canvas" (without quotes).
- Copy the following code into the canvas class.
Java Code:
package java.awt;import eos.Main;import eos.listeners.PaintListener;import javax.accessibility.Accessible;import javax.accessibility.AccessibleContext;import javax.accessibility.AccessibleRole;import java.awt.image.BufferStrategy;import java.awt.image.BufferedImage;import java.awt.peer.CanvasPeer;/**
* Created by Brandon on 2015-12-20.
*/public class Canvas extends Component implements Accessible { private static final long serialVersionUID
= -2284879212465893870L
; private static int nameCounter
= 0; private static final String base
= "canvas"; private BufferedImage debugImage
; private transient PaintListener paintListener
; public Canvas() { Main.
notifyCanvasReady(this); } public Canvas(GraphicsConfiguration config
) { this(); setGraphicsConfiguration
(config
); } public void addPaintListener
(PaintListener listener
) { paintListener
= (PaintListener
)AWTEventMulticaster.
addInternal(paintListener, listener
); } @Override
public Graphics getGraphics
() { if (debugImage
== null || debugImage.
getWidth() != getWidth
() || debugImage.
getHeight() != getHeight
()) { debugImage
= new BufferedImage(getWidth
(), getHeight
(),
BufferedImage.
TYPE_INT_RGB); } Graphics g
= debugImage.
getGraphics(); if (paintListener
!= null) { paintListener.
onPaint(g
); } super.
getGraphics().
drawImage(debugImage,
0,
0,
null); return g
; } @Override
public void setVisible
(boolean visible
) { super.
setVisible(visible
); } @Override
void setGraphicsConfiguration
(GraphicsConfiguration gc
) { synchronized(getTreeLock
()) { CanvasPeer peer
= (CanvasPeer
)getPeer
(); if (peer
!= null) { gc
= peer.
getAppropriateGraphicsConfiguration(gc
); } super.
setGraphicsConfiguration(gc
); } } @Override
String constructComponentName
() { synchronized (Canvas.
class) { return base
+ nameCounter
++; } } @Override
public void addNotify
() { synchronized (getTreeLock
()) { if (peer
== null) peer
= getToolkit
().
createCanvas(this); super.
addNotify(); } } @Override
public void paint
(Graphics g
) { g.
clearRect(0,
0, width, height
); } @Override
public void update
(Graphics g
) { g.
clearRect(0,
0, width, height
); paint
(g
); } @Override
boolean postsOldMouseEvents
() { return true; } @Override
public void createBufferStrategy
(int numBuffers
) { super.
createBufferStrategy(numBuffers
); } @Override
public void createBufferStrategy
(int numBuffers, BufferCapabilities caps
) throws AWTException { super.
createBufferStrategy(numBuffers, caps
); } @Override
public BufferStrategy getBufferStrategy
() { return super.
getBufferStrategy(); } @Override
public AccessibleContext getAccessibleContext
() { if (accessibleContext
== null) { accessibleContext
= new AccessibleAWTCanvas
(); } return accessibleContext
; } protected class AccessibleAWTCanvas
extends AccessibleAWTComponent
{ private static final long serialVersionUID
= -6325592262103146699L
; @Override
public AccessibleRole getAccessibleRole
() { return AccessibleRole.
CANVAS; } }}
Now we have a Canvas that uses our PaintListener. This canvas is the same code as the Official Java.AWT.Canvas class. The only difference is that we've added a few noticeable things to it.
We have add a buffer to the canvas so we can draw on and we have added a PaintListener property:
Java Code:
private BufferedImage debugImage
;private transient PaintListener paintListener
;
Next we have added a function to addPaintListeners to our canvas..
Java Code:
public void addPaintListener
(PaintListener listener
) { paintListener
= (PaintListener
)AWTEventMulticaster.
addInternal(paintListener, listener
);}
Now you're probably wondering what in the hell is a Multicaster. The multi-caster is a CHAIN of listeners. How does it work?
First you supply it the listener that you have (your one listener property). Then you supply it the listener that you want to ADD to the chain of observers. Finally, the value that is returned is the original head of the listener chain.
It's pretty much a linked list of observers.
This way, you keep track of ONE listener and whenever an event occurs, you notify the observer. The multi-caster will automatically notify ALL observers in the chain.
For example:
Java Code:
private LinkedList
<EventListener
> listeners
;public void addInternal
(EventListener a,
EventListener b
) { if (a
== null) { listeners.
add(b
); return b
; } listeners.
add(b
); return a
;}
Now lets say you did:
Java Code:
a
= addInternal
(a, b
);a
= addInternal
(a, c
);a
= addInternal
(a, d
);//Your chain would look like://a -> b -> c -> d.//If you did:a.
notify();//It would translate to (notifying all observers in the observer chain):EventListener e
= a
;while (e.
peek() != null) { e.
notifyObserver(); e
= e.
next();}
This means you can add as many paint listeners as you like and each of them can perform different drawings. Usually you only need one paint listener, but whatever floats your boat..
Next we modified the Canvas constructor:
Java Code:
public Canvas() { Main.
notifyCanvasReady(this);}
This notifies our main class that we are ready to proceed. A canvas has been created and needs us to add listeners/observers to it .
Finally, we have added/overrode one last method in the client:
Java Code:
@Override
public Graphics getGraphics
() { if (debugImage
== null || debugImage.
getWidth() != getWidth
() || debugImage.
getHeight() != getHeight
()) { debugImage
= new BufferedImage(getWidth
(), getHeight
(),
BufferedImage.
TYPE_INT_RGB); } Graphics g
= debugImage.
getGraphics(); if (paintListener
!= null) { paintListener.
onPaint(g
); } super.
getGraphics().
drawImage(debugImage,
0,
0,
null); return g
;}
The client calls this function to draw on the canvas. We need to notify our observers and also give them a proper graphics context that they can use for drawing on.
Voila, we have now completed our PaintListener observer.
Drawing using our PaintListener/PaintObserver.
To draw on our Canvas, we need to add a listener/observer to the canvas. To do this, we modify the addPaintListeners function in the Main file as follows:
Java Code:
public static void notifyCanvasReady
(Canvas canvas
) { canvas.
addPaintListener((PaintListener
) g
-> { g.
setColor(Color.
white); g.
drawString("HELLO WORLD",
50,
50); });}
For those of you that are NOT familiar with the Java Lambda syntax, the above function is the same as:
Java Code:
public static void notifyCanvasReady
(Canvas canvas
) { canvas.
addPaintListener(new PaintListener
(Graphics g
) { g.
setColor(Color.
white); g.
drawString("HELLO WORLD",
50,
50); });}
What this does is that it adds a paintListener to the Canvas which is component 0 on the applet. That's right, the canvas is the very first component of the applet.
We add a listener to it that draws "HELLO WORLD" in the white colour at Point(50, 50).
The results of your intuition is:
http://i.imgur.com/oEx1qEk.jpg