Page 1 of 6 123 ... LastLast
Results 1 to 25 of 128

Thread: Java: How To Create Your Own Bot Interface

  1. #1
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default Java: How To Create Your Own Bot Interface

    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):

    1. Click the file menu, hit New -> Project.
    2. Select Java from the left menu.
    3. Hit Next.
    4. Give your project a name (Ex: BotTutorial).
    5. Hit done.



    The below images are the results of the above steps:

    http://i.imgur.com/Bv2wDHa.png
    http://i.imgur.com/CS3Vwv5.png


    1. Right-Click on the src folder. Hit New -> Package.
    2. Name your package (without quote): "eos".
    3. Right-Click on the src folder. Hit New -> Package.
    4. Name your package (without quotes): "java.awt".
    5. Right-Click on the eos package. Hit New -> Class.
    6. Name your class "Main".
    7. 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).

    1. Click the button with the arrow pointing DOWN at the top right of your IDE. This is the configuration button.
    2. Hit Edit Configurations...
    3. Hit the plus sign at the top left corner of your IDE.
    4. Choose Application.
    5. Give your configuration a name (without quotes): "Main".
    6. Specify your main class (without quotes): "eos.Main".
    7. 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"
    8. Hit Apply.
    9. 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.


    1. Right-Click the eos package. Hit New -> Class.
    2. Enter "BootLoader" as the class name.
    3. 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.


    1. Click the file menu -> Project Structure.
    2. Choose Artifacts from the left menu.
    3. Hit the plus sign and choose Jar -> From Modules With Dependencies.
    4. Specify the main class (without quotes): "eos.BootLoader".
    5. Hit OK.
    6. Put a checkmark in "Build On Make".
    7. Change the name of the Artifact to "Eos" (without quotes).
    8. Hit Apply.
    9. 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.

    1. Right-Click the eos package. Create a new package called "utility" (without quotes).
    2. Right-Click the utility package. Create a new class called "Util" (without quotes).
    3. 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).

    1. Right click the utility package. Hit New -> Class.
    2. 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.


    1. Right-Click the eos package.
    2. Hit New -> Class.
    3. Name the class ClientApplet.
    4. 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.

    1. Go to your Main file.
    2. 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.


    1. Right-Click the eos package.
    2. Hit New -> Package.
    3. Name it "listeners" (without quotes).
    4. Right-Click the listeners package.
    5. Hit New -> Class.
    6. Name the class: "PaintListener" (without quotes).
    7. 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.


    1. Right-Click the java.awt package. (Please note this is NOT the eos package!).
    2. Hit New -> Class.
    3. Name your class "Canvas" (without quotes).
    4. 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
    Attached Files Attached Files
    Last edited by Brandon; 12-21-2015 at 02:04 AM.
    I am Ggzz..
    Hackintosher

  2. #2
    Join Date
    Sep 2010
    Posts
    5,762
    Mentioned
    136 Post(s)
    Quoted
    2739 Post(s)

    Default

    Awww we gotta actually write the code this will be useful one day when I'm not a total noob at java

  3. #3
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default

    Quote Originally Posted by Officer Barbrady View Post
    Awww we gotta actually write the code this will be useful one day when I'm not a total noob at java
    LOL! Hey.. No fair.. I had to write it lol.. Meh I attached file files at the end of the tutorial if you're lazy :l
    I am Ggzz..
    Hackintosher

  4. #4
    Join Date
    Sep 2010
    Posts
    5,762
    Mentioned
    136 Post(s)
    Quoted
    2739 Post(s)

    Default

    Quote Originally Posted by Brandon View Post
    LOL! Hey.. No fair.. I had to write it lol.. Meh I attached file files at the end of the tutorial if you're lazy :l
    I'll might aswell write it have to learn it eventually

  5. #5
    Join Date
    Aug 2007
    Location
    Colorado
    Posts
    7,421
    Mentioned
    268 Post(s)
    Quoted
    1442 Post(s)

    Default

    This is a wonderful tutorial Brandon, really thorough in the explanations. I hope to learn quite a bit after reading this over. Thank you very much for this!

    Current projects:
    [ AeroGuardians (GotR minigame), Motherlode Miner, Blast furnace ]

    "I won't fall in your gravity. Open your eyes,
    you're the Earth and I'm the sky..."


  6. #6
    Join Date
    Oct 2011
    Location
    Chicago
    Posts
    3,352
    Mentioned
    21 Post(s)
    Quoted
    437 Post(s)

    Default

    Great tutorial man!




    Anti-Leech Movement Prevent Leeching Spread the word
    Insanity 60 Days (Killer workout)
    XoL Blog (Workouts/RS/Misc)

  7. #7
    Join Date
    Mar 2012
    Location
    127.0.0.1
    Posts
    3,383
    Mentioned
    95 Post(s)
    Quoted
    717 Post(s)

    Default

    Thanks for the tutorial man. Helps a lot.

    Now lets see if I can pull NKNBot out of the dirt.

  8. #8
    Join Date
    Dec 2011
    Location
    The Netherlands
    Posts
    1,631
    Mentioned
    47 Post(s)
    Quoted
    254 Post(s)

    Default

    Great tutorial, Brandon. I've been rather busy the last few weeks and haven't worked on my own client for atleast 2 months. The last time I worked on it I was working on the layout and trying to get one JTabbedPane working. It did actually work.. sort of but when selecting something from my JMenuBar it would go under the JTabbedPane due to light weight and heavy weight components..

    The ClientPool is really interesting because I wasn't sure how to implement the drawing functions properly when working with multiple tabs. I will need to rewrite a lot to properly be able to add new clients when the user wants more tabs. It's interesting though, I'll work on it in a few weeks when the 4th semester is done and my summer holiday starts I guess..

    Do you have any experience with a JToolBar in combination with a JTabbedPane holding the applets?

    Script source code available here: Github

  9. #9
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default

    Quote Originally Posted by J J View Post
    Great tutorial, Brandon. I've been rather busy the last few weeks and haven't worked on my own client for atleast 2 months. The last time I worked on it I was working on the layout and trying to get one JTabbedPane working. It did actually work.. sort of but when selecting something from my JMenuBar it would go under the JTabbedPane due to light weight and heavy weight components..

    The ClientPool is really interesting because I wasn't sure how to implement the drawing functions properly when working with multiple tabs. I will need to rewrite a lot to properly be able to add new clients when the user wants more tabs. It's interesting though, I'll work on it in a few weeks when the 4th semester is done and my summer holiday starts I guess..

    Do you have any experience with a JToolBar in combination with a JTabbedPane holding the applets?

    Yup. My own bot has a toolbar. Also the Smart I've been working on also has a toolbar with a good amount of buttons and functionality.

    See here: http://villavu.com/forum/showthread.php?t=102735
    I am Ggzz..
    Hackintosher

  10. #10
    Join Date
    Dec 2011
    Location
    The Netherlands
    Posts
    1,631
    Mentioned
    47 Post(s)
    Quoted
    254 Post(s)

    Default

    Quote Originally Posted by Brandon View Post
    Yup. My own bot has a toolbar. Also the Smart I've been working on also has a toolbar with a good amount of buttons and functionality.

    See here: http://villavu.com/forum/showthread.php?t=102735
    Oh yeah, I'll check the source out, haven't done that yet.
    I've been spending most of my time on two university projects for this semester.. actually so much that I have 18 hours on the four man group project where you needed to make about 10 :P So this weekend I will be spending some time on RuneScape related stuff again.

    My goal is to make a multi tabbed loader that supportes different 'types' of RuneScape.

    Have a welcome screen where you can select
    RuneScape classic, RuneScape or RuneScape 2007 in a Combobox.
    Then another combobox where you can select the language and that listens to the other choice. So if RuneScape is selected you can select an option, otherwise it is disabled.

    Once you press on new tab it should show that same interface again and have support to load english, german clients in different tabs without problems..

    Nice challenge. Will also need to make use of ClientPools

    EDIT: Made a nice start today. Learned more about Tabs. I can now hit the + button to create a new tab with a custom panel attached. Tomorrow I'll work on the actual loading part for English RuneScape with a Client Pool.. :P

    Last edited by J J; 05-24-2013 at 08:31 PM.

    Script source code available here: Github

  11. #11
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default

    Quote Originally Posted by J J View Post
    Oh yeah, I'll check the source out, haven't done that yet.
    I've been spending most of my time on two university projects for this semester.. actually so much that I have 18 hours on the four man group project where you needed to make about 10 :P So this weekend I will be spending some time on RuneScape related stuff again.

    My goal is to make a multi tabbed loader that supportes different 'types' of RuneScape.

    Have a welcome screen where you can select
    RuneScape classic, RuneScape or RuneScape 2007 in a Combobox.
    Then another combobox where you can select the language and that listens to the other choice. So if RuneScape is selected you can select an option, otherwise it is disabled.

    Once you press on new tab it should show that same interface again and have support to load english, german clients in different tabs without problems..

    Nice challenge. Will also need to make use of ClientPools

    EDIT: Made a nice start today. Learned more about Tabs. I can now hit the + button to create a new tab with a custom panel attached. Tomorrow I'll work on the actual loading part for English RuneScape with a Client Pool.. :P

    Looking nice! As soon as you finish your code and stuff, then you can pretty up the UI and stuff. I can see it now. That's a good start!
    I am Ggzz..
    Hackintosher

  12. #12
    Join Date
    Dec 2011
    Location
    The Netherlands
    Posts
    1,631
    Mentioned
    47 Post(s)
    Quoted
    254 Post(s)

    Default

    Quote Originally Posted by Brandon View Post
    Looking nice! As soon as you finish your code and stuff, then you can pretty up the UI and stuff. I can see it now. That's a good start!
    I got the loading with tabs to work, except for replacing the Canvas. It just doesn't seem to work.
    My .bat: java -Xmx1024m -Xbootclasspath/p:"Canvas.jar" -jar Loader.jar
    I exported the Canvas as a normal .jar file.

    The error:

    Code:
    C:\Users\JanJaap\Desktop>java -Xmx1024m -Xbootclasspath/p:"Canvas.jar" -jar Load
    er.jar
    Start button pressed!
    Chosen version: NORMAL, chosen language: ENGLISH
    Creating client
    End of ClientApplet
    Starting applet
    Waiting for canvas @ StartPanel
    adding client @ StartPanel
    Adding client! [StartPanel]
    Adding client applet
    [Canvas] getGraphics()
    Client == null: null
    Exception in thread "AWT-EventQueue-0" java.lang.NoClassDefFoundError: org/obdur
    o/loader/utils/ClientPool
            at java.awt.Canvas.getGraphics(Canvas.java:47)
            at sun.awt.RepaintArea.paint(Unknown Source)
            at sun.awt.windows.WComponentPeer.handleEvent(Unknown Source)
            at java.awt.Component.dispatchEventImpl(Unknown Source)
            at java.awt.Component.dispatchEvent(Unknown Source)
            at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
            at java.awt.EventQueue.access$200(Unknown Source)
            at java.awt.EventQueue$3.run(Unknown Source)
            at java.awt.EventQueue$3.run(Unknown Source)
            at java.security.AccessController.doPrivileged(Native Method)
            at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Sour
    ce)
            at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Sour
    ce)
            at java.awt.EventQueue$4.run(Unknown Source)
            at java.awt.EventQueue$4.run(Unknown Source)
            at java.security.AccessController.doPrivileged(Native Method)
            at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Sour
    ce)
            at java.awt.EventQueue.dispatchEvent(Unknown Source)
            at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
            at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
            at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
            at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
            at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
            at java.awt.EventDispatchThread.run(Unknown Source)
    [Canvas] getGraphics()
    Client == null: null
    Exception in thread "" java.lang.NoClassDefFoundError: org/obduro/loader/utils/C
    lientPool
            at java.awt.Canvas.getGraphics(Canvas.java:47)
            at jo.i(jo.java:108)
            at jn.run(jn.java:93)
            at java.lang.Thread.run(Unknown Source)
    [Canvas] getGraphics()
    Client == null: null
    Exception in thread "AWT-EventQueue-0" java.lang.NoClassDefFoundError: org/obdur
    o/loader/utils/ClientPool
            at java.awt.Canvas.getGraphics(Canvas.java:47)
            at sun.awt.RepaintArea.paint(Unknown Source)
            at sun.awt.windows.WComponentPeer.handleEvent(Unknown Source)
            at java.awt.Component.dispatchEventImpl(Unknown Source)
            at java.awt.Component.dispatchEvent(Unknown Source)
            at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
            at java.awt.EventQueue.access$200(Unknown Source)
            at java.awt.EventQueue$3.run(Unknown Source)
            at java.awt.EventQueue$3.run(Unknown Source)
            at java.security.AccessController.doPrivileged(Native Method)
            at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Sour
    ce)
            at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Sour
    ce)
            at java.awt.EventQueue$4.run(Unknown Source)
            at java.awt.EventQueue$4.run(Unknown Source)
            at java.security.AccessController.doPrivileged(Native Method)
            at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Sour
    ce)
            at java.awt.EventQueue.dispatchEvent(Unknown Source)
            at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
            at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
            at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)[C
    anvas] getGraphics()
    
    Client == null: null    at java.awt.EventDispatchThread.pumpEvents(Unknown Sourc
    e)
    
            at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
            at java.awt.EventDispatchThread.run(Unknown Source)
    Error: java.awt.Canvas.getGraphics:47 alc.y:35 ym.s:92 dl.t:89 ep.i:135 am.fx:18
    76 acu.fe:1850 ng.fj:1843 da.j:564 r.i:240 client.ar:1377 nk.m:582 nk.o:558 nk.r
    un:515 java.lang.Thread.run | java.lang.NoClassDefFoundError: org/obduro/loader/
    utils/ClientPool |  0,0,0,0 0,0,0 0 0 2 1,1 0 0 0 0 0 0 911 15 0 MRBQmhx4tVpYeDb
    wMzzCJcPuXzF5Yy3b
    error_game_crash

    If I use your arguments java -jar Loader.jar -Xbootclasspath/p:"Loader.jar it won't replace the canvas.

    EDIT: Forgot a " in the args, now I have
    java -jar Loader.jar -Xbootclasspath/p:"Loader.jar"
    Still not replacing the Canvas though

    And the Canvas class relies on ClientPool and some others so I assume you really have to xboot with yourself

    EDIT 2: Yay, I got it working after doing some investigation...


    Correct arg:

    java -Xbootclasspath/p:Loader.jar -jar Loader.jar


    When you click on new tab you get this screen


    Then when you click on start it loads. Multiple tabs do also work properly


    Now to make it close properly by destructing the applet. Also need to override the JFrame closing that it destroys all applets.

    EDIT 3:
    Can you give more information about multi threading for tabs? I'm just using one thread right now. I have worked with multiple threads on Android but not with actual Java.
    Last edited by J J; 05-25-2013 at 07:01 PM.

    Script source code available here: Github

  13. #13
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default

    Quote Originally Posted by J J View Post
    EDIT 3:
    Can you give more information about multi threading for tabs? I'm just using one thread right now. I have worked with multiple threads on Android but not with actual Java.

    Glad you got the Canvas working. The reason for the error in the first place is because you exported it to another jar. If you export it, then you need to export all dependencies too.. aka client pool and all its dependencies and so on and so forth. Else, place the canvas in the same jar and x-boot with yourself like you discovered


    For the threading what I did was I have two functions for adding a Tab to the JTabbedPane.

    One just adds it which forces all other clients to wait until the applet initialized. The next function uses a thread but a synchronized one that synchronizes on JTabbedPane.

    The reason is because JTabbedPane and most components can only process events ONE at a time. You can't shove a shit ton of events from multiple threads at the same time without giving it time to update the UI.


    So to destroy an Applet/Tab so that you don't get stupid exceptions or crash or lag and lower CPU usage for each one destroyed, what I did was:

    • First Remove the Tab so users don't destroy the same tab twice if they're lagging or w/e.
    • Second Remove the Tab from the EventBlocker. This queue blocks events to the canvas so its only right we stop blocking from a canvas that's about to be destroyed.
    • Third, Remove the Applet from the JPanel (loader)
    • Stop and Destroy the applet.
    • Remove the Clients from the client pool.
    • System GC or just null the client/applet reference.


    Thus:

    Java Code:
    public void addClient(Client client) {  //Just in case we don't care about multiple clients/tabs..
        this.removeSplashScreen();

        synchronized (this.TabPane) {
            this.addTab(TabPane, "Tab-ID: [" + this.getFreeTabID() + "]", client.getLoader()); //Add the LOADER to the Tab. You can add the client but loader is easier to handle. Its a JPanel.
            this.TabPane.setSelectedIndex(this.TabPane.getTabCount() - 1);
            this.updateUI();  //Update the TabUI quickly.
           
            if (this.TabPane.getTabCount() == 1) { //If its our first time adding a client, meh.. center the window and pack.
                this.pack();
                this.setSize(this.getWidth(), this.getHeight()); //Because I removed the splashScreen so client needs resizing..
                this.centerWindow();
            }
        }
    }


    public void addTabbedClient(final Client client) { //Used for multiple tabs where each tab has its own thread so as to not affect the other tabs :)
        new Thread(new Runnable() {
            @Override
            public void run() {
                Frame.this.addClient(client);

                            //Process other stuff such as Waiting on Listeners to register upon addition if need be..
                            //Only waits for listeners IF the Loader has "started" NOT added as that'd LOCK all threads..
            }
        }).start();
    }


    //Cleaning up is a bit tricky but I did it like this:

    public void removeClient(final ClientApplet loader) {   //Given a loader, remove we need to synchronize the JTabbedPane thread so as to not remove all clients at the EXACT same time. That will lock it up.
        if (loader == null || loader.getApplet() == null || loader.getApplet().getComponentCount() < 1) {
            return;
        }

        synchronized (this.TabPane) {  //Lock only the JTabbedPane.
            this.TabPane.remove(loader); //Remove the loader and unlock it so other threads can do the same. We remove the tab first because you don't want users destructing one tab TWICE!
        }

        try {
            new Thread(new Runnable() {   //If we are only closing One tab, we want to close it via a thread so as to not lag the rest of the clients while the UI updates itself.
                @Override
                public void run() {
                    Client client = null;
                    synchronized (clients) {  //If you need to synchronize your clientpool so that multiple threads don't delete from it at the same time then do so otherwise ignore this line.
                        if (loader.getApplet() != null && loader.getApplet().getComponentCount() > 0) {  //If our loader is not already destroyed and has a valid canvas (if it has a canvas, that means RS has loaded)
                            EventHandler.remove(loader.getApplet().getComponent(0));  //Remove Our ComponentBlocking.. Basically No point of blocking input on a canvas that's being destroyed.
                            client = clients.get(loader.getApplet().getComponent(0).getClass().getClassLoader().hashCode());
                            clients.remove(loader.getApplet().getComponent(0).getClass().getClassLoader().hashCode()); //Remove the client from the ClientPool.
                        }
                    }

                    loader.remove(loader.getApplet());  //This is the tricky part. It must be in order! First we need to remove the applet from the JPanel using this line.
                    loader.getApplet().destroy();  //Next we destroy the applet.
                    loader.destruct();             //This function calls applet.stop() followed by applet.destroy(). Then it sets applet to null and closes the ClassLoader.
                    Main.decreaseTabCount();       //Meh needed for Smart JNI.
                    if (client != null) {
                        client.destruct();         //Needed for Smart JNI which destroyed the MemoryMap File and cleans up the stupid SMART.PID files.
                        client = null;             //Allow garbage collection to do its thing if need be. No references left :)
                    }
                }
            }).start();
        } catch (Exception Ex) {
            Main.StackTrace(Ex);
        }
    }

    private void removeAllClients() {  //YOU DO NOT WANT TO THREAD THIS! Why? Because your application can close before the thread has time to complete its job.
        synchronized (this.TabPane) {    //No need for this but just in case someone used it wrong and spawned two frames in the same client.
            this.TabPane.removeAll();  //First Remove ALL TABS!.
        }

        synchronized (clients) {
            for (Client client : clients.values()) {  //Loop through all clients and destroy them.
                if (client != null && client.getLoader() != null && client.getCanvas() != null) {
                    EventHandler.remove(client.getCanvas());  //Unblock input from that Tab.
                    //clients.remove(client.getCanvas().getClass().getClassLoader().hashCode());
                    client.getLoader().remove(client.getApplet());  //remove the applet from the JPanel.
                    client.getApplet().destroy();  //Destruct the applet.
                    client.destruct();     //destruct the loader.
                    Main.decreaseTabCount();
                }
            }
            clients.clear(); //Clear our client pool all at once.
        }
        System.gc();    //Let garbage collection do its thing if need be rather than setting all null to invoke it.
    }


    //YOU DO NOT WANT TO THREAD THIS! Why? Because your application can close before the thread has time to complete its job.
    public void destruct() {      //Added this to Frame.onWindowClosing so that the frame first goes invisible then destructs the clients. It stops users from trying to destruct TWICE by double clicking the exit button..
        Frame.this.setVisible(false);
        Frame.this.removeAllClients();
        Main.cleanUp();
        System.exit(0);
    }
    Last edited by Brandon; 05-26-2013 at 01:20 AM.
    I am Ggzz..
    Hackintosher

  14. #14
    Join Date
    Apr 2013
    Location
    Las Vegas
    Posts
    111
    Mentioned
    1 Post(s)
    Quoted
    35 Post(s)

    Default

    When you finish making the oldschool client with tabs could you post the finished product?

    An oldschool client with tabs would be awesome. thanks

  15. #15
    Join Date
    Mar 2012
    Location
    127.0.0.1
    Posts
    3,383
    Mentioned
    95 Post(s)
    Quoted
    717 Post(s)

    Default

    @Brandon

    XBootClassPath is to set your canvas as the superclass of theirs, right?

    What if you got an instance of their canvas class, and edited the bytecode to redirect it to yours.

    Would this eliminate the need for Xbootclasspath?

  16. #16
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default

    Quote Originally Posted by NKN View Post
    @Brandon

    XBootClassPath is to set your canvas as the superclass of theirs, right?

    What if you got an instance of their canvas class, and edited the bytecode to redirect it to yours.

    Would this eliminate the need for Xbootclasspath?
    This works as well. You can also tell if they are inheriting from your canvas class by setting yours to final. You will notice that it throws an error because they can't inherit from it. So yeah. Injecting your own canvas into the client is an alternative to X-booting.
    I am Ggzz..
    Hackintosher

  17. #17
    Join Date
    Mar 2012
    Location
    127.0.0.1
    Posts
    3,383
    Mentioned
    95 Post(s)
    Quoted
    717 Post(s)

    Default

    Quote Originally Posted by Brandon View Post
    This works as well. You can also tell if they are inheriting from your canvas class by setting yours to final. You will notice that it throws an error because they can't inherit from it. So yeah. Injecting your own canvas into the client is an alternative to X-booting.
    Alright cool. Trying to get it so you can run the jar file, and not have to use a .bat file.

  18. #18
    Join Date
    Jan 2012
    Posts
    1,104
    Mentioned
    18 Post(s)
    Quoted
    211 Post(s)

    Default

    Thanks alot for this!

    Can someone help me with OSRS parameters? I get this error:
    Code:
    java.lang.ClassNotFoundException: client
    These are the strings I get with regex:
    Code:
    Archive = gamepack_2653253.jar
    JarLocation = http://oldschool1.runescape.com//gamepack_2653253.jar
    Code = client

  19. #19
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default

    Quote Originally Posted by Shatterhand View Post
    Thanks alot for this!

    Can someone help me with OSRS parameters? I get this error:
    Code:
    java.lang.ClassNotFoundException: client
    These are the strings I get with regex:
    Code:
    Archive = gamepack_2653253.jar
    JarLocation = http://oldschool1.runescape.com//gamepack_2653253.jar
    Code = client
    forgot I even had a tutorial on this :S


    The // in your url is preventing it from finding the right jar.

    The jarLocation should be: http://oldschool1.runescape.com/gamepack_2653253.jar

    Your applet will load after that. This basically means that you have a url ending in a slash. Remove the trailing slash and it'll be all good.


    Oh and if you want some copy-pasta then the following should still work (hopefully):

    ClientApplet.java
    Java Code:
    package PackageForNKN;

    import java.applet.*;
    import java.awt.*;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.net.*;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map.Entry;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import javax.swing.JOptionPane;
    import javax.swing.JPanel;

    public final class ClientApplet extends JPanel implements AppletStub {

        private static final long serialVersionUID = 5627929030422355843L;
        private Applet applet = null;
        private URLClassLoader ClassLoader = null;
        private URL codeBase = null, documentBase = null;
        private HashMap<String, String> parameters = new HashMap<>();
        private static final Pattern codeRegex = Pattern.compile("code=(.*?) ");
        private static final Pattern archiveRegex = Pattern.compile("archive=(.*?) ");
        private static final Pattern parameterRegex = Pattern.compile("<param name=\"([^\\s]+)\"\\s+value=\"([^>]*)\">");
       
        public ClientApplet(String root, String params, int Width, int Height) {
            try {
                this.setLayout(new BorderLayout(0, 0));
                String pageSource = downloadPage(new URL(root + params), null);
                Matcher codeMatcher = codeRegex.matcher(pageSource);
                Matcher archiveMatcher = archiveRegex.matcher(pageSource);

                if (codeMatcher.find() && archiveMatcher.find()) {
                    String archive = archiveMatcher.group(1);
                    String jarLocation = root + "/" + archive;
                    String code = codeMatcher.group(1).replaceAll(".class", "");
                    Matcher parameterMatcher = parameterRegex.matcher(pageSource);
                    this.codeBase = new URL(jarLocation);
                    this.documentBase = new URL(root);

                    while (parameterMatcher.find()) {
                        this.parameters.put(parameterMatcher.group(1), parameterMatcher.group(2));
                    }
                   
                    this.ClassLoader = new URLClassLoader(new URL[]{new URL(jarLocation)});
                    this.applet = (Applet) ClassLoader.loadClass(code).newInstance();
                    this.applet.setStub(this);
                    this.applet.setPreferredSize(new Dimension(Width, Height));
                    this.add(this.applet, BorderLayout.CENTER);
                }
            } catch (MalformedURLException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
                JOptionPane.showMessageDialog(null, "Error Loading.. Please Check Your Internet Connection.", "Error Loading..", JOptionPane.ERROR_MESSAGE);
            }
        }
       
        public ClassLoader getClassLoader() {
            return this.ClassLoader;
        }
       
        public String postRequest(URL Address, String UserAgent, HashMap<String, String> data) {
            StringBuilder parameterBuilder = new StringBuilder();
            Iterator<Entry<String, String>> it = data.entrySet().iterator();
            int i = 0;

            while(it.hasNext() && i < data.entrySet().size() - 1) {
                Entry<String, String> e = it.next();
                parameterBuilder.append(e.getKey()).append("=").append(e.getValue()).append("&");
                ++i;
            }
            Entry<String, String> entry = it.next();
            parameterBuilder.append(entry.getKey()).append("=").append(entry.getValue());
           
            String parameterURL = parameterBuilder.toString();
            if (parameterURL == null || parameterURL.trim().isEmpty()) {
                return null;
            }
           
            try {
                URLConnection Connection = Address.openConnection();
                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"));
                }
               
                Connection.setDoOutput(true);
               
               
                try (OutputStreamWriter writer = new OutputStreamWriter(Connection.getOutputStream())) {
                    writer.write(parameterURL);
                    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");
                        }
                    }
                    return Builder.toString();
                }
            } catch(Exception e) {
                e.printStackTrace();
            }
            return null;
        }
       
        public String downloadPage(URL Address, String UserAgent) {
            try {
                URLConnection Connection = Address.openConnection();
                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"));
                }

                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");
                    }
                }
                return Builder.toString();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }

        public Applet getApplet() {
            return this.applet;
        }

        public void start() {
            if (this.applet != null) {
                this.applet.init();
            }

            if (this.applet != null) {
                this.applet.start();
            }
        }

        public void destruct() {
            if (this.applet != null) {
                this.remove(this.applet);
                this.applet.stop();
                this.applet.destroy();
                this.applet = null;
            }
        }

        @Override
        public boolean isActive() {
            return true;
        }

        @Override
        public URL getDocumentBase() {
            return this.documentBase;
        }

        @Override
        public URL getCodeBase() {
            return this.codeBase;
        }

        @Override
        public String getParameter(String name) {
            return this.parameters.get(name);
        }

        @Override
        public AppletContext getAppletContext() {
            return null;
        }

        @Override
        public void appletResize(int width, int height) {
        }
    }

    Main.java:
    Java Code:
    package PackageForNKN;

    public class Main {
        public static void main(String[] args) {
            new ClientApplet("http://oldschool1.runescape.com", "", 765, 503);
        }
    }
    Last edited by Brandon; 01-01-2014 at 11:56 PM.
    I am Ggzz..
    Hackintosher

  20. #20
    Join Date
    Jan 2012
    Posts
    1,104
    Mentioned
    18 Post(s)
    Quoted
    211 Post(s)

    Default

    Quote Originally Posted by Brandon View Post
    forgot I even had a tutorial on this :S


    The // in your url is preventing it from finding the right jar.

    The jarLocation should be: http://oldschool1.runescape.com/gamepack_2653253.jar

    Your applet will load after that. This basically means that you have a url ending in a slash. Remove the trailing slash and it'll be all good.


    Oh and if you want some copy-pasta then the following should still work (hopefully):

    ClientApplet.java
    Java Code:
    package PackageForNKN;

    import java.applet.*;
    import java.awt.*;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.net.*;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map.Entry;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import javax.swing.JOptionPane;
    import javax.swing.JPanel;

    public final class ClientApplet extends JPanel implements AppletStub {

        private static final long serialVersionUID = 5627929030422355843L;
        private Applet applet = null;
        private URLClassLoader ClassLoader = null;
        private URL codeBase = null, documentBase = null;
        private HashMap<String, String> parameters = new HashMap<>();
        private static final Pattern codeRegex = Pattern.compile("code=(.*?) ");
        private static final Pattern archiveRegex = Pattern.compile("archive=(.*?) ");
        private static final Pattern parameterRegex = Pattern.compile("<param name=\"([^\\s]+)\"\\s+value=\"([^>]*)\">");
       
        public ClientApplet(String root, String params, int Width, int Height) {
            try {
                this.setLayout(new BorderLayout(0, 0));
                String pageSource = downloadPage(new URL(root + params), null);
                Matcher codeMatcher = codeRegex.matcher(pageSource);
                Matcher archiveMatcher = archiveRegex.matcher(pageSource);

                if (codeMatcher.find() && archiveMatcher.find()) {
                    String archive = archiveMatcher.group(1);
                    String jarLocation = root + "/" + archive;
                    String code = codeMatcher.group(1).replaceAll(".class", "");
                    Matcher parameterMatcher = parameterRegex.matcher(pageSource);
                    this.codeBase = new URL(jarLocation);
                    this.documentBase = new URL(root);

                    while (parameterMatcher.find()) {
                        this.parameters.put(parameterMatcher.group(1), parameterMatcher.group(2));
                    }
                   
                    this.ClassLoader = new URLClassLoader(new URL[]{new URL(jarLocation)});
                    this.applet = (Applet) ClassLoader.loadClass(code).newInstance();
                    this.applet.setStub(this);
                    this.applet.setPreferredSize(new Dimension(Width, Height));
                    this.add(this.applet, BorderLayout.CENTER);
                }
            } catch (MalformedURLException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
                JOptionPane.showMessageDialog(null, "Error Loading.. Please Check Your Internet Connection.", "Error Loading..", JOptionPane.ERROR_MESSAGE);
            }
        }
       
        public ClassLoader getClassLoader() {
            return this.ClassLoader;
        }
       
        public String postRequest(URL Address, String UserAgent, HashMap<String, String> data) {
            StringBuilder parameterBuilder = new StringBuilder();
            Iterator<Entry<String, String>> it = data.entrySet().iterator();
            int i = 0;

            while(it.hasNext() && i < data.entrySet().size() - 1) {
                Entry<String, String> e = it.next();
                parameterBuilder.append(e.getKey()).append("=").append(e.getValue()).append("&");
                ++i;
            }
            Entry<String, String> entry = it.next();
            parameterBuilder.append(entry.getKey()).append("=").append(entry.getValue());
           
            String parameterURL = parameterBuilder.toString();
            if (parameterURL == null || parameterURL.trim().isEmpty()) {
                return null;
            }
           
            try {
                URLConnection Connection = Address.openConnection();
                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"));
                }
               
                Connection.setDoOutput(true);
               
               
                try (OutputStreamWriter writer = new OutputStreamWriter(Connection.getOutputStream())) {
                    writer.write(parameterURL);
                    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");
                        }
                    }
                    return Builder.toString();
                }
            } catch(Exception e) {
                e.printStackTrace();
            }
            return null;
        }
       
        public String downloadPage(URL Address, String UserAgent) {
            try {
                URLConnection Connection = Address.openConnection();
                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"));
                }

                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");
                    }
                }
                return Builder.toString();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }

        public Applet getApplet() {
            return this.applet;
        }

        public void start() {
            if (this.applet != null) {
                this.applet.init();
            }

            if (this.applet != null) {
                this.applet.start();
            }
        }

        public void destruct() {
            if (this.applet != null) {
                this.remove(this.applet);
                this.applet.stop();
                this.applet.destroy();
                this.applet = null;
            }
        }

        @Override
        public boolean isActive() {
            return true;
        }

        @Override
        public URL getDocumentBase() {
            return this.documentBase;
        }

        @Override
        public URL getCodeBase() {
            return this.codeBase;
        }

        @Override
        public String getParameter(String name) {
            return this.parameters.get(name);
        }

        @Override
        public AppletContext getAppletContext() {
            return null;
        }

        @Override
        public void appletResize(int width, int height) {
        }
    }

    Main.java:
    Java Code:
    package PackageForNKN;

    public class Main {
        public static void main(String[] args) {
            new ClientApplet("http://oldschool1.runescape.com", "", 765, 503);
        }
    }
    You were right about the double "//", but you pasted wrong code here.

    Changed this line and it works now. Thanks!
    Code:
    String jarLocation = root + archive;

  21. #21
    Join Date
    Dec 2013
    Posts
    95
    Mentioned
    2 Post(s)
    Quoted
    37 Post(s)

    Default

    Thanks for posting this! I was really interested in this idea for another game.

    How would you modify these instructions for a game such as Yohoho Puzzle Pirates? YPP is a pure-Java game. Its loader is a signed Java applet which then launches a large JFrame with (what appear to be, from the appearance of the scrollbars etc.) lots of Swing components inside comprising the main game GUI. How would you obtain a reference to the JFrame owned by the applet? What about its children?

    Also, could you explain how to send fake input to the game, please?

  22. #22
    Join Date
    Feb 2007
    Location
    PA, USA
    Posts
    5,240
    Mentioned
    36 Post(s)
    Quoted
    496 Post(s)

    Default

    THANK YOU BRANDON! I'm trying to write my own JAVA Color Bot

  23. #23
    Join Date
    Feb 2011
    Location
    The Future.
    Posts
    5,600
    Mentioned
    396 Post(s)
    Quoted
    1598 Post(s)

    Default

    Quote Originally Posted by vwxz View Post
    Thanks for posting this! I was really interested in this idea for another game.

    How would you modify these instructions for a game such as Yohoho Puzzle Pirates? YPP is a pure-Java game. Its loader is a signed Java applet which then launches a large JFrame with (what appear to be, from the appearance of the scrollbars etc.) lots of Swing components inside comprising the main game GUI. How would you obtain a reference to the JFrame owned by the applet? What about its children?

    Also, could you explain how to send fake input to the game, please?
    Oh geez.. this game is extremely hard to hook. Trust me I tried hooking their OpenGL code through a wrapper, its impossible. You can only inject into that game and even then, it's hard too because they link to their libraries statically.. That means it's already linked as soon as the process even starts. I had to create the process in suspended mode, re-write their stupid tables and then resume the thread.. None of which was done in Java.

    MAYBE using a loader would be different and far easier.

    As for your other questions, the information to load an applet is in the OP. You can create a JFrame and then place the applet within the frame. If the game uses a canvas then all you do is make it use yours as shown in the OP as well. This would allow you to get colours from it.

    If it uses a Component instead of a canvas, then you'll either have to replace that component with your own, or grab the colours from the Graphics2D.



    Puzzle pirates loads its own JFrame though. All its data is stored in /users/appdata/puzzlepirates. Something like that.
    If using the steam client, it uses OpenGL otherwise it uses Graphics2D and can be used through Simba. I never tried making a loader for it.
    Last edited by Brandon; 01-02-2014 at 04:34 PM.
    I am Ggzz..
    Hackintosher

  24. #24
    Join Date
    Jan 2012
    Posts
    1,104
    Mentioned
    18 Post(s)
    Quoted
    211 Post(s)

    Default

    How do you set up Keylistener or Keybindings for the applet?

    Keylistener doesnt work at all. When I add it to the applet, nothing happens when I fire an event. When I add it the the Frame, nothing happens when the focus is on the applet, even thought after setFocusable(false) on the applet or the ClientApplet.

    Keybindings only works with WHEN_IN_FOCUSED_WINDOW and when focus is on another JComponent. I tried playing with the focus (disabling, enabling, requesting) but I cant find the solution.

  25. #25
    Join Date
    Mar 2012
    Location
    127.0.0.1
    Posts
    3,383
    Mentioned
    95 Post(s)
    Quoted
    717 Post(s)

    Default

    Quote Originally Posted by Shatterhand View Post
    How do you set up Keylistener or Keybindings for the applet?

    Keylistener doesnt work at all. When I add it to the applet, nothing happens when I fire an event. When I add it the the Frame, nothing happens when the focus is on the applet, even thought after setFocusable(false) on the applet or the ClientApplet.

    Keybindings only works with WHEN_IN_FOCUSED_WINDOW and when focus is on another JComponent. I tried playing with the focus (disabling, enabling, requesting) but I cant find the solution.
    I think you'll have to override the applets default keylisteners.
    This is what I did for PulseBot
    Code:
    package org.pulsebot.injection.input;
    
    import org.pulsebot.injection.generic.RSClient;
    
    import java.awt.*;
    import java.awt.event.*;
    import java.util.ArrayList;
    import java.util.Arrays;
    
    /**
     * Created with IntelliJ IDEA.
     * User: NKN
     * Date: 6/18/13
     * Time: 9:28 PM
     */
    public class PulseKeyListeners implements KeyListener {
        private ArrayList<KeyListener> keyListeners = null;
        private Canvas canvas = null;
        private RSClient rsClient = null;
        private Boolean InputEnabled = true;
    
        public PulseKeyListeners(Canvas canvas, RSClient rsClient) {
            this.construct(canvas, rsClient);
        }
    
        public void construct(Canvas canvas, RSClient client) {
            this.canvas = canvas;
            this.rsClient = rsClient;
            this.keyListeners = new ArrayList<>();
            this.keyListeners.addAll(Arrays.asList(canvas.getKeyListeners()));
            removeAllListeners();
        }
    
        public void updateRSClientCanvas(Canvas canvas, RSClient RSClient) {
            this.restoreAllListeners();
            this.construct(canvas, RSClient);
        }
    
        private void removeAllListeners() {
            for (KeyListener Listener : canvas.getKeyListeners()) {
                canvas.removeKeyListener(Listener);
            }
            canvas.addKeyListener(this);
        }
    
        private void restoreAllListeners() {
            canvas.removeKeyListener(this);
    
            for (KeyListener Listener : keyListeners) {
                canvas.addKeyListener(Listener);
            }
    
        }
    
        @Override
        public void keyTyped(KeyEvent e) {
            if (e.getSource() == rsClient.getApplet()) {
                e.setSource(canvas);
                for (KeyListener Listener : keyListeners) {
                    Listener.keyTyped(e);
                }
            } else if (InputEnabled) {
                for (KeyListener Listener : keyListeners) {
                    Listener.keyTyped(e);
                }
            }
        }
    
        @Override
        public void keyPressed(KeyEvent e) {
            if (e.getSource() == rsClient.getApplet()) {
                e.setSource(canvas);
    
                for (KeyListener Listener : keyListeners) {
                    Listener.keyPressed(e);
                }
            } else if (InputEnabled) {
                for (KeyListener Listener : keyListeners) {
                    Listener.keyPressed(e);
                }
            }
        }
    
        @Override
        public void keyReleased(KeyEvent e) {
            if (e.getSource() == rsClient.getApplet()) {
                e.setSource(canvas);
    
                for (KeyListener Listener : keyListeners) {
                    Listener.keyReleased(e);
                }
            } else if (InputEnabled) {
                for (KeyListener Listener : keyListeners) {
                    Listener.keyReleased(e);
                }
            }
        }
    }

Page 1 of 6 123 ... LastLast

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •