Welcome to the Java Programming Forums


The professional, friendly Java community. 21,500 members and growing!


The Java Programming Forums are a community of Java programmers from all around the World. Our members have a wide range of skills and they all have one thing in common: A passion to learn and code Java. We invite beginner Java programmers right through to Java professionals to post here and share your knowledge. Become a part of the community, help others, expand your knowledge of Java and enjoy talking with like minded people. Registration is quick and best of all free. We look forward to meeting you.


>> REGISTER NOW TO START POSTING


Members have full access to the forums. Advertisements are removed for registered users.

Results 1 to 7 of 7

Thread: video game problem - delay in response to arrow key presses

  1. #1
    Member
    Join Date
    Oct 2010
    Posts
    31
    Thanks
    2
    Thanked 1 Time in 1 Post

    Default video game problem - delay in response to arrow key presses

    In my video game I'm programming, I'm experiencing a bit of a delay in response to key presses. I'm hoping someone can help me out. Once in a while whenever I move the main character around, there seems to be a half-second delay between when I press the arrow key and when the character actually moves. I'm not sure why.

    First, let me post my key binding method. This goes into the GamePanel class I created, which is the main JPanel that displays the game graphics. I give the ActionMap my InputProcessor class, which mainly deals with static variables, but also has a few non-static variables so that each KeyStroke can be associated with its own unique InputProcessor.

        public void bindKeys() {
     
    	// variable declarations:
    	InputMap inputMap   = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    	ActionMap actionMap = getActionMap();
     
    	int PRESSED  = InputProcessor.PRESSED;
    	int RELEASED = InputProcessor.RELEASED;
     
    	int UP       = KeyEvent.VK_UP;
    	int RIGHT    = KeyEvent.VK_RIGHT;
    	int DOWN     = KeyEvent.VK_DOWN;
    	int LEFT     = KeyEvent.VK_LEFT;
     
    	int N 	     = KeyEvent.VK_N; // new maze
    	int R	     = KeyEvent.VK_R; // restart level
    	int Q	     = KeyEvent.VK_Q; // quit
    	int P	     = KeyEvent.VK_P; // pause
     
    	// up arrow key
    	inputMap.put(KeyStroke.getKeyStroke(UP, 0, false), "up_pressed");
    	actionMap.put("up_pressed", new InputProcessor(PRESSED, UP));
    	inputMap.put(KeyStroke.getKeyStroke(UP, 0, true), "up_released");
    	actionMap.put("up_released", new InputProcessor(RELEASED, UP));	
     
    	// right arrow key
    	inputMap.put(KeyStroke.getKeyStroke(RIGHT, 0, false), "right_pressed");
    	actionMap.put("right_pressed", new InputProcessor(PRESSED, RIGHT));
    	inputMap.put(KeyStroke.getKeyStroke(RIGHT, 0, true), "right_released");
    	actionMap.put("right_released", new InputProcessor(RELEASED, RIGHT));
     
    	// down arrow key
    	inputMap.put(KeyStroke.getKeyStroke(DOWN, 0, false), "down_pressed");
    	actionMap.put("down_pressed", new InputProcessor(PRESSED, DOWN));
    	inputMap.put(KeyStroke.getKeyStroke(DOWN, 0, true), "down_released");
    	actionMap.put("down_released", new InputProcessor(RELEASED, DOWN));
     
    	// left arrow key
    	inputMap.put(KeyStroke.getKeyStroke(LEFT, 0, false), "left_pressed");
    	actionMap.put("left_pressed", new InputProcessor(PRESSED, LEFT));
    	inputMap.put(KeyStroke.getKeyStroke(LEFT, 0, true), "left_released");
    	actionMap.put("left_released", new InputProcessor(RELEASED, LEFT));
     
    	// 'n' (for new maze)
    	inputMap.put(KeyStroke.getKeyStroke(N, 0, false), "n_pressed");
    	actionMap.put("n_pressed", new InputProcessor(PRESSED, N));
     
    	// 'r' (for restart maze)
    	inputMap.put(KeyStroke.getKeyStroke(R, 0, false), "r_pressed");
    	actionMap.put("r_pressed", new InputProcessor(PRESSED, R));
     
    	// 'q' (for quit)
    	inputMap.put(KeyStroke.getKeyStroke(Q, 0, false), "q_pressed");
    	actionMap.put("q_pressed", new InputProcessor(PRESSED, Q));
     
    	// 'p' (for pause)
    	inputMap.put(KeyStroke.getKeyStroke(P, 0, false), "p_pressed");
    	actionMap.put("p_pressed", new InputProcessor(PRESSED, P));
        }

    Here's the InputProcessor class (sorry for the length):

    package game;
     
    import javax.swing.AbstractAction;
     
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
     
    import java.util.Date;
     
    public class InputProcessor extends AbstractAction {
     
     
    /************************** EMBEDDED CLASS **************************/
     
        /* The TimedKeyEvent class is to be placed in the timedKeyEventArray (below)
         * and represents a key having been pressed.
         */
     
        private class TimedKeyEvent {
     
    	public long    time_stamp = -1;	   // when key was pressed
    	public boolean held_down  = false; // is it being held down?
    	public int     key_code   = -1;	   // which key was it? (-1 = no key pressed)
     
    	public TimedKeyEvent(int kc, boolean hd) {
     
    	    key_code   = kc;
    	    held_down  = hd;
    	    time_stamp = new Date().getTime();
    	}
        }
     
    /************************** END EMBEDDED CLASS **************************/
     
        // key codes
        public static final int UP    = KeyEvent.VK_UP;
        public static final int RIGHT = KeyEvent.VK_RIGHT;
        public static final int DOWN  = KeyEvent.VK_DOWN;
        public static final int LEFT  = KeyEvent.VK_LEFT;
     
        public static final int N	  = KeyEvent.VK_N; // new maze
        public static final int R	  = KeyEvent.VK_R; // restart level
        public static final int Q	  = KeyEvent.VK_Q; // quit
        public static final int P	  = KeyEvent.VK_P; // pause game
     
        // array to hold TimedKeyEvents. To be placed at index = TimedKeyEvent.key_code.
        // To be indexed using index_array (in case above key codes aren't consecutive on some systems
        // helm points to oldest TimedKeyEvent (among the arrow keys)
     
        public static final int      ARRAY_SIZE       = 256;
        private static TimedKeyEvent timedKeyEvents[] = new TimedKeyEvent[ARRAY_SIZE];
        private static int 	  	 helm 		  = -1;
        private int 	  	 index_array[]    = {UP, RIGHT, DOWN, LEFT};
     
        // true when GameControl needs to be notified when an arrow key has been released
        // (for enabling GoodGuy's movement)
        private static boolean notify_on_key_release = false;
     
        private static GameControl gameControl;
     
        // what type of key event was it?
        public final static int PRESSED  = 1;
        public final static int RELEASED = 2;
        public final static int TYPED    = 3;
        private final int key_event_type;
     
        // which key was it?
        private final int key_code;
     
     
    /************************** CONSTRUCTOR **************************/
     
        // to be used for key binding only (doesn't initialize TimedKeyEvents array)
        public InputProcessor(int type, int code) {
     
    	if (type != PRESSED && type != RELEASED && type != TYPED) {
    	    System.out.println("type = " + type + " in InputProcessor constructor, which is invalid.");
    	}
     
    	key_event_type = type;
    	key_code = code;
        }
     
    /************************** END CONSTRUCTOR **************************/
     
     
    /************************** ACTION PERFORMED **************************/
     
        public void actionPerformed(ActionEvent actionEvent) {
     
    	// arrow keys
    	if (key_code == UP || key_code == RIGHT || 
    	   key_code == DOWN || key_code == LEFT) {
    	    if (key_event_type == PRESSED) {
    	        timedKeyEvents[key_code] = new TimedKeyEvent(key_code, true);
    		if (helm == -1) updateHelm();
    	    } else if (key_event_type == RELEASED) {
    		timedKeyEvents[key_code] = null;
    		if (helm == key_code) updateHelm();
    	    }
    	}
     
    	// 'n' (for new maze)
    	if (key_code == N) {
    	    gameControl.getGameFrame().getEastPanel().actionPerformed(new ActionEvent(
    		gameControl.getGameFrame().getEastPanel().getNewMazeButton(), 0, ""));
    	}
     
    	// 'r' (for restart level)
    	if (key_code == R) {
    	    gameControl.getGameFrame().getEastPanel().actionPerformed(new ActionEvent(
    		gameControl.getGameFrame().getEastPanel().getRestartLevelButton(), 0, ""));
    	}
     
    	// 'q' (for quit)
    	if (key_code == Q) {
    	    gameControl.getGameFrame().getEastPanel().actionPerformed(new ActionEvent(
    		gameControl.getGameFrame().getEastPanel().getQuitButton(), 0, ""));
    	}
     
    	// 'p' (for pause)
    	if (key_code == P) {
    	    if (gameControl.getGameState() == gameControl.PAUSED)
    		gameControl.unpause();
    	    else gameControl.pause();
    	}
     
    	// notify GameControl that key has been released (in case GoodGuy's movements needs enabling)
    	if (key_event_type == RELEASED && notify_on_key_release) {
    	    if (gameControl.notify(key_code))
    	        notify_on_key_release = false; // only set to false if GoodGuy DID need enabling (depends on which key it was)
    	}
        }
     
    /************************** END ACTION PERFORMED **************************/
     
     
    /************************** HELM CONTROL **************************/
     
        // updates helm to point at arraw key with oldest time stamp
        private void updateHelm() {
     
    	long oldest_time_stamp;
     
    	// helm = -1 means not pointing at any key
    	// figure out if an arrow key is held down and point to it (should be AT MOST one)
    	if (helm == -1) {
     
    	    if (timedKeyEvents[RIGHT] != null) helm = RIGHT;
    	    if (timedKeyEvents[DOWN]  != null) helm = DOWN;
    	    if (timedKeyEvents[LEFT]  != null) helm = LEFT;
    	    if (timedKeyEvents[UP]    != null) helm = UP;
     
    	// if hilm != -1, see if key it points at is null
    	// if so, find remaining arrow key with oldest time stamp
    	// initialize helm to -1 so that if no arrow keys found,
    	// it will remain at -1 indicating no arrow keys are down.
     
    	} else if (timedKeyEvents[helm] == null) {
     
    	    oldest_time_stamp = new Date().getTime();
    	    helm = -1;
     
    	    for (int i = 0; i < 4; i++)
    		if (timedKeyEvents[index_array[i]] != null &&
    		    timedKeyEvents[index_array[i]].time_stamp < oldest_time_stamp) {
    		    oldest_time_stamp = timedKeyEvents[index_array[i]].time_stamp;
    		    helm = index_array[i];
    		}
    	}
     
    	// if helm isn't pointing to null, no need to change it
        }
     
    /************************** END HELM CONTROL **************************/
     
     
    /************************** ACCESSORS **************************/
     
    /*** DOES/IS ***/
     
         public static boolean doesRightHaveHelm() {
     
    	if (helm == RIGHT)
    	    return timedKeyEvents[helm].held_down;
    	else return false;
        }
     
    /////////////
     
        public static boolean doesDownHaveHelm() {
     
    	if (helm == DOWN)
    	    return timedKeyEvents[helm].held_down;
    	else return false;
        }
     
    /////////////
     
        public static boolean doesLeftHaveHelm() {
     
    	if (helm == LEFT)
    	    return timedKeyEvents[helm].held_down;
    	else return false;
        }
     
    /////////////
     
        public static boolean doesUpHaveHelm() {
     
    	if (helm == UP)
    	    return timedKeyEvents[helm].held_down;
    	else return false;
        }
     
    /////////////
     
    // this might pose problems for key typed events (ex. when traversing ballchain)
        public static boolean isArrowKeyPressed() {
     
    	if (helm == RIGHT || helm == DOWN || helm == LEFT || helm == UP)
    	    return true;
    	return false;
        }
     
    /*** RESET ***/
     
        // to be used ONLY for reseting key typed events for arrow keys
        public static void resetArrowKeyTyped() {
     
    	timedKeyEvents[helm] = null;
    	helm = -1;	
        }
     
    /*** SET ***/
     
        public static void setGameControl(GameControl gc) {
     
    	if (gc == null) {
    	    System.out.println("gc is null in InputProcessor.setGameControl().");
    	}
     
    	gameControl = gc;
        }
     
    /////////////
     
        public static void setNotifyOnKeyRelease() {notify_on_key_release = true;}
     
    /*** INIT ***/
     
        public static void initVariables() {
     
    	// initialize all TimedKeyEvents to null
    	for (int i = 0; i < ARRAY_SIZE; i++)
    	    timedKeyEvents[i] = null;
        }
     
    /************************** END ACCESSORS **************************/
     
    }

    I'll briefly explain the InputProcessor class.

    It deals with a static array of TimedKeyEvents. A TimedKeyEvent is a class I created and embedded in the InputProcessor. It basically represents a key being pressed and records the time at which it was pressed. When a key is pressed, a TimedKeyEvent is created and stored in the array at the index equal to its key code. When the key is released, that index is set to null.

    Another static variable of importance is helm, an int that points to whichever TimedKeyEvent (out of the arrow keys only) is the oldest. This way, while holding down some arrow key, the player can simultaneously press down another arrow key without interfering with the main character's movement (it will basically ignore the newer key press). If the newer key press is released before the older key press is released, it will be totally ignored. However, if the older key press is released before the newer one is, then the helm will search for and point to the newer one, thereby moving the main character in that direction (essentially, the helm looks for the TimedKeyEvent (out of the arrow keys) with the oldest time stamp).

    Finally, there are the does*HaveHelm() methods (doesRightHaveHelm(), doesLeftHaveHelm(),...). These are the methods called from the main character's move() method. If they return true, that means that arrow key * has the helm, and the main character should move in that direction.

    This move() method is called once ever millisecond. That is to say, the main game loop, which includes a call to move(), is called in response to a Timer set to fire an ActionEvent every millisecond. This is where things get confusing. The delay I'm trying to debug definitely lasts longer than a millisecond (more like 500 milliseconds, or half a second). I don't see anywhere where a delay should occur. As soon as an arrow key is pressed, a TimedKeyEvent should immediately be created and stored in the array, and the helm should immediately point to it. Then within a millisecond, the main character's move() method should be called, and within it, a call to does*HaveHelm() should be made. It should return true in the case when * is the arrow key that has just been pressed, and at this point helm should already point to it. This should all occur in less than a millisecond.

    Can anyone see what the problem is?

    I can offer a few leads: 1) the delay usually happens when the release of one key occurs almost simultaneously with the pressing of another (the one that experiences a delay). This tells me the problem *might* have something to do with the helm searching for the TimedKeyEvent with the oldest time stamp in response to the one it last pointed at being released, but I still don't see where the delay could be coming from. 2) I've only ever experienced this problem on my laptop, which is a lot slower than my desktop on which I haven't experienced this problem at all.

    Any help would be greatly appreciated.


  2. #2
    Super Moderator helloworld922's Avatar
    Join Date
    Jun 2009
    Posts
    2,896
    Thanks
    23
    Thanked 619 Times in 561 Posts
    Blog Entries
    18

    Default Re: video game problem - delay in response to arrow key presses

    This is because of the way most OS's handle keypress events.

    Try this: open up a notepad (or some other text editor). Then press and hold a letter key down. Notice how you get one letter, then about half a second later or so it finally starts repeating letters?

    This same phenomenon is caused by the OS sending one key event, but not sending another because it thinks you're just typing and doesn't want you to accidentally type duplicate letters. However, this behavior generally isn't desirable in games.

    So, what I usually do is listen for the key down event and mark a flag in my program saying that a certain key is down. In my main program loop, I'll check that flag and if the key is marked as down, do some action (say, move left). Once the user takes their finger off that key, your OS will send another event which means the key was unpressed. At this point, the program will reset the flag, and the main program loop will see this and not move the character left until that flag is set again.

  3. #3
    Member
    Join Date
    Oct 2010
    Posts
    31
    Thanks
    2
    Thanked 1 Time in 1 Post

    Default Re: video game problem - delay in response to arrow key presses

    Quote Originally Posted by helloworld922 View Post
    So, what I usually do is listen for the key down event and mark a flag in my program saying that a certain key is down. In my main program loop, I'll check that flag and if the key is marked as down, do some action (say, move left). Once the user takes their finger off that key, your OS will send another event which means the key was unpressed. At this point, the program will reset the flag, and the main program loop will see this and not move the character left until that flag is set again.
    But this is exactly what my InputProcessor does. As soon as an arrow key is pressed, a TimeKeyEvent is stored in the array and helm points to it. Then in the main game loop, it checks for any arrow key being held down by checking if helm points to it. As soon as the key is release, the entry in the array where the TimedKeyEvent was is set to null, and the helm looks for the oldest TimedKeyEvent (for the arrow keys) and points to it (kind of like a queue). If it can't find any (i.e. no arrow keys are being held down) it gets set to -1.

    If the problem was the key-repeat issue, the delay would happen every time; but it doesn't. It only happens once in a while (and only on my laptop).

  4. #4
    Super Moderator helloworld922's Avatar
    Join Date
    Jun 2009
    Posts
    2,896
    Thanks
    23
    Thanked 619 Times in 561 Posts
    Blog Entries
    18

    Default Re: video game problem - delay in response to arrow key presses

    ::blush:: I didn't look too carefully over the code and kind of assumed that's what you did (my bad). Their are only two other things I can think of:

    1. You might have a "race condition" somewhere inside your code (might occur if your program uses multi-threading)
    2. Your laptop may be pausing. My laptop does this where every now and then it will freeze up for maybe a second or so (in my case, it's because my hard disk sucks). I kind of doubt this is the case, but it could be.

    So you've tried this on other computers and it doesn't do this?

  5. #5
    Member
    Join Date
    Oct 2010
    Posts
    31
    Thanks
    2
    Thanked 1 Time in 1 Post

    Default Re: video game problem - delay in response to arrow key presses

    Quote Originally Posted by helloworld922 View Post
    1. You might have a "race condition" somewhere inside your code (might occur if your program uses multi-threading)
    That's what I suspect. From what I undestand, key bindings respond asynchronously, and if you read (in my OP) the first condition under which this delay usually occurs, it makes sense that some race condition is happening - namely, releasing one key simultaneously with pressing another. It could be that the processing of the key-release event is undoing/preventing the changes that are supposed to occur with the processing of the key-pressed event.

    Problem is, I'm still unsure how to resolve this problem. I've heard of the synchronize keyword which *I think* is supposed to handle these kinds of situations, but I've tried to use it in the past without success (I need to understand it better).

    If you're still willing to help me, maybe you can do a couple things for me. First, can you suggest a quick online tutorial on how to use the synchronize keyword? And second, can you suggest where the best place in my code above would be for wrapping a synchronize block around? I'm guessing it would be in the actionPerformed() method of my InputProcessor - specifically the code that handles arrow keys:

        public void actionPerformed(ActionEvent actionEvent) {
     
        // arrow keys
        if (key_code == UP || key_code == RIGHT || 
           key_code == DOWN || key_code == LEFT) {
            if (key_event_type == PRESSED) {
                timedKeyEvents[key_code] = new TimedKeyEvent(key_code, true);
            if (helm == -1) updateHelm();
            } else if (key_event_type == RELEASED) {
            timedKeyEvents[key_code] = null;
            if (helm == key_code) updateHelm();
            }
        }
     
    // ...
    }

    Either that, or the updateHelm() method:

        // updates helm to point at arraw key with oldest time stamp
        private void updateHelm() {
     
    	long oldest_time_stamp;
     
    	// helm = -1 means not pointing at any key
    	// figure out if an arrow key is held down and point to it (should be AT MOST one)
    	if (helm == -1) {
     
    	    if (timedKeyEvents[RIGHT] != null) helm = RIGHT;
    	    if (timedKeyEvents[DOWN]  != null) helm = DOWN;
    	    if (timedKeyEvents[LEFT]  != null) helm = LEFT;
    	    if (timedKeyEvents[UP]    != null) helm = UP;
     
    	// if hilm != -1, see if key it points at is null
    	// if so, find remaining arrow key with oldest time stamp
    	// initialize helm to -1 so that if no arrow keys found,
    	// it will remain at -1 indicating no arrow keys are down.
     
    	} else if (timedKeyEvents[helm] == null) {
     
    	    oldest_time_stamp = new Date().getTime();
    	    helm = -1;
     
    	    for (int i = 0; i < 4; i++)
    		if (timedKeyEvents[index_array[i]] != null &&
    		    timedKeyEvents[index_array[i]].time_stamp < oldest_time_stamp) {
    		    oldest_time_stamp = timedKeyEvents[index_array[i]].time_stamp;
    		    helm = index_array[i];
    		}
    	}
     
    	// if helm isn't pointing to null, no need to change it
        }

  6. #6
    Super Moderator helloworld922's Avatar
    Join Date
    Jun 2009
    Posts
    2,896
    Thanks
    23
    Thanked 619 Times in 561 Posts
    Blog Entries
    18

    Default Re: video game problem - delay in response to arrow key presses

    You can start by looking at Intrinsic Locks and Synchronization (The Java™ Tutorials > Essential Classes > Concurrency). There are other tutorials which precede/follow this one, you can take a look at those if you want.

    Basically, synchronized blocks allow you to obtain the mutex lock for a specific object. Once you leave that synchronized block, I believe the mutex is automatically released.

    Theoretically, you should provide a synchronize block everywhere you have code which could access the same object from multiple threads. It's much easier to be safe than to try and debug multi-threading issues If there's a section where you get detrimental performance hits because of synchronization (for example, if you sleep the thread inside a synchronize block), you may want to consider re-factoring your code, or see if you could do without a synchronize block.

    There's another way you can provide synchronization using wait() and notify(), but I'm not too familiar with this method.

  7. #7
    Member
    Join Date
    Oct 2010
    Posts
    31
    Thanks
    2
    Thanked 1 Time in 1 Post

    Default Re: video game problem - delay in response to arrow key presses

    Thank helloworld,

    I tried a couple things:

        public void actionPerformed(ActionEvent actionEvent) {
     
    	synchronized (this) {
     
    	    // arrow keys
    	    if (key_code == UP || key_code == RIGHT || 
    	       key_code == DOWN || key_code == LEFT) {
    	    	if (key_event_type == PRESSED) {
    	            timedKeyEvents[key_code] = new TimedKeyEvent(key_code, true);
    		    if (helm == -1) updateHelm();
    	        } else if (key_event_type == RELEASED) {
    		    timedKeyEvents[key_code] = null;
    		    if (helm == key_code) updateHelm();
    	    	}
    	    }
    	}
    //...

    and this:

        private synchronized void updateHelm() {
     
    	long oldest_time_stamp;
     
    	// helm = -1 means not pointing at any key
    	// figure out if an arrow key is held down and point to it (should be AT MOST one)
    	if (helm == -1) {
     
    	    if (timedKeyEvents[RIGHT] != null) helm = RIGHT;
    	    if (timedKeyEvents[DOWN]  != null) helm = DOWN;
    	    if (timedKeyEvents[LEFT]  != null) helm = LEFT;
    	    if (timedKeyEvents[UP]    != null) helm = UP;
     
    	// if hilm != -1, see if key it points at is null
    	// if so, find remaining arrow key with oldest time stamp
    	// initialize helm to -1 so that if no arrow keys found,
    	// it will remain at -1 indicating no arrow keys are down.
     
    	} else if (timedKeyEvents[helm] == null) {
     
    	    oldest_time_stamp = new Date().getTime();
    	    helm = -1;
     
    	    for (int i = 0; i < 4; i++)
    		if (timedKeyEvents[index_array[i]] != null &&
    		    timedKeyEvents[index_array[i]].time_stamp < oldest_time_stamp) {
    		    oldest_time_stamp = timedKeyEvents[index_array[i]].time_stamp;
    		    helm = index_array[i];
    		}
    	}
     
    	// if helm isn't pointing to null, no need to change it
        }

    Unfortunately, neither of them worked. I still get the delay either way.

Similar Threads

  1. simple game with swing and awt problem
    By Pulse_Irl in forum AWT / Java Swing
    Replies: 2
    Last Post: October 12th, 2010, 02:04 PM
  2. jmf , video with hopes of AR
    By wolfgar in forum Java SE APIs
    Replies: 0
    Last Post: December 4th, 2009, 03:54 AM
  3. Delay/Wait/Pause in Java + AirBrushing
    By obliza in forum What's Wrong With My Code?
    Replies: 6
    Last Post: October 27th, 2009, 10:27 AM
  4. [SOLVED] minesweeper game creation problem
    By kaylors in forum What's Wrong With My Code?
    Replies: 5
    Last Post: June 27th, 2009, 04:06 PM
  5. [SOLVED] Java for loop problem and out put is not coming
    By thewonderdude in forum Loops & Control Statements
    Replies: 9
    Last Post: March 15th, 2009, 02:31 PM