From b66f0ec24591c2482b25e6c0380dc543dba4dab3 Mon Sep 17 00:00:00 2001 From: forteri76 Date: Mon, 28 Apr 2014 16:58:44 +0000 Subject: [PATCH] Added network multiplayer, and fixed numerous bugs. --- AndroidManifest.xml | 127 +- res/drawable/round_button.xml | 21 + res/layout/activity_preferences_screen.xml | 12 +- res/layout/activity_splash_screen.xml | 4 +- res/values-de/strings.xml | 2 +- res/values-es/strings.xml | 2 +- res/values-fr/strings.xml | 2 +- res/values-zh-rCN/strings.xml | 2 +- res/values/strings.xml | 2 +- .../frozenbubble/AccelerometerManager.java | 1 + src/com/efortin/frozenbubble/CRC16.java | 149 ++ src/com/efortin/frozenbubble/ComputerAI.java | 85 +- src/com/efortin/frozenbubble/HighscoreDB.java | 1 + src/com/efortin/frozenbubble/HighscoreDO.java | 1 + .../frozenbubble/HighscoreManager.java | 11 +- src/com/efortin/frozenbubble/HomeScreen.java | 599 +++++- src/com/efortin/frozenbubble/ModPlayer.java | 26 +- .../frozenbubble/MulticastManager.java | 575 ++++++ .../frozenbubble/NetworkGameManager.java | 1809 +++++++++++++++++ src/com/efortin/frozenbubble/Preferences.java | 120 ++ .../frozenbubble/PreferencesActivity.java | 180 +- .../frozenbubble/ScrollingCredits.java | 31 +- .../frozenbubble/ScrollingTextView.java | 4 +- .../frozenbubble/SeekBarPreference.java | 2 +- .../efortin/frozenbubble/VirtualInput.java | 168 ++ .../andmodplug/MODResourcePlayer.java | 124 +- .../andmodplug/PlayerThread.java | 472 ++--- src/org/gsanson/frozenbubble/Freile.java | 67 +- src/org/gsanson/frozenbubble/MalusBar.java | 85 +- src/org/jfedor/frozenbubble/BubbleFont.java | 2 +- src/org/jfedor/frozenbubble/BubbleSprite.java | 14 +- src/org/jfedor/frozenbubble/Compressor.java | 21 +- src/org/jfedor/frozenbubble/FrozenBubble.java | 1057 +++++----- src/org/jfedor/frozenbubble/FrozenGame.java | 1415 +++++++------ src/org/jfedor/frozenbubble/GameScreen.java | 35 +- src/org/jfedor/frozenbubble/GameView.java | 271 ++- src/org/jfedor/frozenbubble/LevelManager.java | 22 +- .../frozenbubble/MultiplayerGameView.java | 1583 +++++++++++---- .../jfedor/frozenbubble/PenguinSprite.java | 9 - 39 files changed, 6778 insertions(+), 2335 deletions(-) create mode 100644 res/drawable/round_button.xml create mode 100644 src/com/efortin/frozenbubble/CRC16.java create mode 100644 src/com/efortin/frozenbubble/MulticastManager.java create mode 100644 src/com/efortin/frozenbubble/NetworkGameManager.java create mode 100644 src/com/efortin/frozenbubble/Preferences.java create mode 100644 src/com/efortin/frozenbubble/VirtualInput.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 0731644..ea8f202 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,61 +1,66 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/round_button.xml b/res/drawable/round_button.xml new file mode 100644 index 0000000..5fe7934 --- /dev/null +++ b/res/drawable/round_button.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/activity_preferences_screen.xml b/res/layout/activity_preferences_screen.xml index 5d31dbe..6925f36 100644 --- a/res/layout/activity_preferences_screen.xml +++ b/res/layout/activity_preferences_screen.xml @@ -1,6 +1,7 @@ - - - - - + android:title="2 Player Game Options" > + + tools:context=".HomeScreen" > Musik Abspielen Spielen Soundeffekte Frozen Bubble - Frozen Bubble + Frozen Bubble "Original Development\n" "------------------------\n" "\nGuillaume Cottenceau - Design and Programming\n" diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 293f843..bae2b8b 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -20,7 +20,7 @@ Tocar Música Tocar Efectos Sonoros Frozen Bubble - Frozen Bubble + Frozen Bubble "Original Development\n" "------------------------\n" "\nGuillaume Cottenceau - Design and Programming\n" diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 25ddd1c..f25b565 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -20,7 +20,7 @@ Jouer Musique Jouer Effets Sonores Frozen Bubble - Frozen Bubble + Frozen Bubble "Développement Original\n" "------------------------\n" "\nGuillaume Cottenceau - Design et Programmation\n" diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 0cbbaa8..877ec4b 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -20,7 +20,7 @@ 奏乐 播放声音效果 冰冻泡泡 - 冰冻泡泡 + 冰冻泡泡 "Original Development\n" "------------------------\n" "\nGuillaume Cottenceau - Design and Programming\n" diff --git a/res/values/strings.xml b/res/values/strings.xml index 358e13a..c6774dd 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -20,7 +20,7 @@ Play Level Music Play Sound Effects Frozen Bubble - Frozen Bubble + Frozen Bubble "Original Development\n" "------------------------\n" "\nGuillaume Cottenceau - Design and Programming\n" diff --git a/src/com/efortin/frozenbubble/AccelerometerManager.java b/src/com/efortin/frozenbubble/AccelerometerManager.java index 00d7440..d20bad1 100644 --- a/src/com/efortin/frozenbubble/AccelerometerManager.java +++ b/src/com/efortin/frozenbubble/AccelerometerManager.java @@ -67,6 +67,7 @@ *
http://www.gnu.org/licenses/gpl-3.0.html * * @author Antoine Vianey + * */ public class AccelerometerManager { private static Sensor sensor; diff --git a/src/com/efortin/frozenbubble/CRC16.java b/src/com/efortin/frozenbubble/CRC16.java new file mode 100644 index 0000000..9664236 --- /dev/null +++ b/src/com/efortin/frozenbubble/CRC16.java @@ -0,0 +1,149 @@ +/* + * [[ Frozen-Bubble ]] + * + * Copyright (c) 2000-2003 Guillaume Cottenceau. + * Java sourcecode - Copyright (c) 2003 Glenn Sanson. + * Additional source - Copyright (c) 2013 Eric Fortin. + * + * This code is distributed under the GNU General Public License + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 or 3, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to: + * Free Software Foundation, Inc. + * 675 Mass Ave + * Cambridge, MA 02139, USA + * + * Artwork: + * Alexis Younes <73lab at free.fr> + * (everything but the bubbles) + * Amaury Amblard-Ladurantie + * (the bubbles) + * + * Soundtrack: + * Matthias Le Bidan + * (the three musics and all the sound effects) + * + * Design & Programming: + * Guillaume Cottenceau + * (design and manage the project, whole Perl sourcecode) + * + * Java version: + * Glenn Sanson + * (whole Java sourcecode, including JIGA classes + * http://glenn.sanson.free.fr/jiga/) + * + * Android port: + * Pawel Aleksander Fedorynski + * Eric Fortin + * Copyright (c) Google Inc. + * + * [[ http://glenn.sanson.free.fr/fb/ ]] + * [[ http://www.frozen-bubble.org/ ]] + */ + +/* + * Copyright 2008 - CommonCrawl Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.efortin.frozenbubble; + +import java.util.zip.Checksum; + +/** + * 16 bit CRC implementation. + * + * @author rana + * + */ +public class CRC16 implements Checksum { + + int crcValue; + + /** + * Create a new CRC16 calculation utility object with the checksum + * initialized to the desired value. + * @param crcStart - the initial CRC value. + */ + public CRC16(int crcStart) { + crcValue = crcStart; + } + + /***************************************************************************/ + // 16bit CRC computation support + /***************************************************************************/ + + static final int[] crc16_table = { 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, + 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, + 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, + 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, + 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, + 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, + 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0, 0x3180, 0xF141, + 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, + 0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, + 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, + 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, + 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, + 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, + 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, + 0xA501, 0x65C0, 0x6480, 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, + 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, + 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, + 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, + 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, + 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, + 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01, 0x5CC0, 0x5D80, 0x9D41, + 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, + 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, + 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, + 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 }; + + @Override + public long getValue() { + return crcValue; + } + + @Override + public void reset() { + crcValue = 0; + } + + @Override + public void update(int b) { + crcValue = (crcValue >>> 8) ^ crc16_table[(crcValue ^ (byte) b) & 0xff]; + } + + @Override + public void update(byte[] b, int off, int len) { + for (int i = off; i < (off + len); ++i) { + crcValue = (crcValue >>> 8) ^ crc16_table[(crcValue ^ b[i]) & 0xff]; + } + } +} diff --git a/src/com/efortin/frozenbubble/ComputerAI.java b/src/com/efortin/frozenbubble/ComputerAI.java index f5500c5..56930da 100644 --- a/src/com/efortin/frozenbubble/ComputerAI.java +++ b/src/com/efortin/frozenbubble/ComputerAI.java @@ -53,7 +53,9 @@ package com.efortin.frozenbubble; import org.gsanson.frozenbubble.Freile; +import org.gsanson.frozenbubble.Freile.eventEnum; import org.jfedor.frozenbubble.FrozenGame; +import org.jfedor.frozenbubble.GameScreen.gameEnum; import android.view.KeyEvent; @@ -62,15 +64,16 @@ public class ComputerAI extends Thread implements Freile.OpponentListener { private boolean running; private FrozenGame myFrozenGame; private Freile cpuOpponent; + private VirtualInput myPlayerInput; /** * Game AI thread class constructor. - * - * @param gameRef - * - reference used to access game information for this player. + * @param gameRef - reference used to access game information for + * this player. */ - public ComputerAI(FrozenGame gameRef) { + public ComputerAI(FrozenGame gameRef, VirtualInput inputRef) { myFrozenGame = gameRef; + myPlayerInput = inputRef; cpuOpponent = new Freile(myFrozenGame.getGrid()); cpuOpponent.setOpponentListener(this); action = 0; @@ -97,37 +100,46 @@ public void clearAction() { } } - public double convertAngleToPosition(double angle) { - double position = (angle - Freile.MIN_LAUNCHER) / - (Freile.MAX_LAUNCHER - Freile.MIN_LAUNCHER); - position = (position * (FrozenGame.MAX_LAUNCH_DIRECTION - - FrozenGame.MIN_LAUNCH_DIRECTION)) + - FrozenGame.MIN_LAUNCH_DIRECTION; - return position; - } - - public double convertPositionToAngle(double position) { - double angle = ((double)(position - FrozenGame.MIN_LAUNCH_DIRECTION)) / - ((double)(FrozenGame.MAX_LAUNCH_DIRECTION - - FrozenGame.MIN_LAUNCH_DIRECTION)); - angle = (angle * (Freile.MAX_LAUNCHER - Freile.MIN_LAUNCHER)) + - Freile.MIN_LAUNCHER; - return angle; + /** + * Convert the supplied value to a normalized launcher position or + * launcher angle as desired. + * @param isAngle - if true, convert to position from + * angle. Otherwise convert to angle from position. + * @param value - the value to convert. + * @return The converted value. + */ + private double convert(boolean isAngle, double value) { + double result; + + if (isAngle) { + result = (value - Freile.MIN_LAUNCHER) / + (Freile.MAX_LAUNCHER - Freile.MIN_LAUNCHER); + result = FrozenGame.MIN_LAUNCH_DIRECTION + + (result * (FrozenGame.MAX_LAUNCH_DIRECTION - + FrozenGame.MIN_LAUNCH_DIRECTION)); + } + else { + result = ((double)(value - FrozenGame.MIN_LAUNCH_DIRECTION)) / + ((double)(FrozenGame.MAX_LAUNCH_DIRECTION - + FrozenGame.MIN_LAUNCH_DIRECTION)); + result = Freile.MIN_LAUNCHER + + (result * (Freile.MAX_LAUNCHER - Freile.MIN_LAUNCHER)); + } + return result; } /** * Return the current state of the opponent action. When the AI has * generated the next action, the action is set to a non-zero value. - * * @return returns the value of the CPU opponent action. */ public int getAction() { return action; } - public void onOpponentEvent(int event) { + public void onOpponentEvent(eventEnum event) { switch (event) { - case Freile.EVENT_DONE_COMPUTING: + case DONE_COMPUTING: synchronized(this) { this.notify(); } @@ -145,19 +157,17 @@ public void run() { /* * Compute the next CPU action. */ - if (running && (myFrozenGame != null) && - (myFrozenGame.getGameResult() == FrozenGame.GAME_PLAYING) && - !cpuOpponent.isComputing()) + if (running && (myFrozenGame != null) && !cpuOpponent.isComputing() && + (myFrozenGame.getGameResult() == gameEnum.PLAYING)) cpuOpponent.compute(myFrozenGame.getCurrentColor(), myFrozenGame.getNextColor(), - myFrozenGame.getCompressorPosition()); + myFrozenGame.getCompressorSteps()); /* * Only fire if the game state permits, and the last virtual * opponent action has been processed. */ - if (running && (myFrozenGame != null) && - myFrozenGame.getOkToFire() && + if (running && (myFrozenGame != null) && myFrozenGame.getOkToFire() && (action != KeyEvent.KEYCODE_DPAD_UP)) { while (running && cpuOpponent.isComputing()) { synchronized(this) { @@ -179,11 +189,13 @@ public void run() { while (running && (myFrozenGame != null) && (actionNew != KeyEvent.KEYCODE_DPAD_UP) && (System.currentTimeMillis() < timeout)) { - actionNew = cpuOpponent.getAction(convertPositionToAngle( - myFrozenGame.getPosition())); + actionNew = cpuOpponent. + getAction(convert(false, myFrozenGame.getPosition())); - if (actionNew != KeyEvent.KEYCODE_DPAD_UP) + if (actionNew != KeyEvent.KEYCODE_DPAD_UP) { action = actionNew; + myPlayerInput.setAction(action, false); + } synchronized(this) { wait(); @@ -195,9 +207,10 @@ public void run() { */ if (running && (myFrozenGame != null) && myFrozenGame.getOkToFire()) { - myFrozenGame.setPosition(convertAngleToPosition( - cpuOpponent.getExactDirection(0))); + myFrozenGame. + setPosition(convert(true, cpuOpponent.getExactDirection(0))); action = actionNew; + myPlayerInput.setAction(action, false); } } @@ -215,8 +228,8 @@ public void run() { /** * Stop the thread run() execution. - *

- * Interrupt the thread when it is suspended via wait(). + *

Interrupt the thread when it is suspended via + * wait(). */ public void stopThread() { running = false; diff --git a/src/com/efortin/frozenbubble/HighscoreDB.java b/src/com/efortin/frozenbubble/HighscoreDB.java index 007b9a4..7bab2c2 100644 --- a/src/com/efortin/frozenbubble/HighscoreDB.java +++ b/src/com/efortin/frozenbubble/HighscoreDB.java @@ -58,6 +58,7 @@ * http://www.screaming-penguin.com/node/7742 * * @author Michel Racic (http://www.2030.tk) + * */ import java.util.ArrayList; import java.util.List; diff --git a/src/com/efortin/frozenbubble/HighscoreDO.java b/src/com/efortin/frozenbubble/HighscoreDO.java index 38f1eeb..5db9744 100644 --- a/src/com/efortin/frozenbubble/HighscoreDO.java +++ b/src/com/efortin/frozenbubble/HighscoreDO.java @@ -55,6 +55,7 @@ /** * @author Michel Racic (http://www.2030.tk) + * */ public class HighscoreDO { private int id; diff --git a/src/com/efortin/frozenbubble/HighscoreManager.java b/src/com/efortin/frozenbubble/HighscoreManager.java index ae96df5..981b666 100644 --- a/src/com/efortin/frozenbubble/HighscoreManager.java +++ b/src/com/efortin/frozenbubble/HighscoreManager.java @@ -60,8 +60,9 @@ import android.os.Bundle; /** + * A class to manage the highscore table for each level. * @author Michel Racic (http://www.2030.tk) - *
A class to manage the highscore table for each level. + * */ public class HighscoreManager { @@ -87,8 +88,9 @@ public void close() { } /** - * @param nbBubbles - * - The number of bubbles launched by the player. + * Take snapshots of the game statistics and store them in a database + * object. + * @param nbBubbles - The number of bubbles launched by the player. */ public void endLevel(int nbBubbles) { long endTime = System.currentTimeMillis(); @@ -146,8 +148,7 @@ public void startLevel(int level) { /** * Accumulate the play time between pause/resume cycles. - *

- * pausedTime is an accumulation of the play time + *

pausedTime is an accumulation of the play time * between pause/resume cycles. */ public void pauseLevel() { diff --git a/src/com/efortin/frozenbubble/HomeScreen.java b/src/com/efortin/frozenbubble/HomeScreen.java index efdbb95..0f12fe6 100644 --- a/src/com/efortin/frozenbubble/HomeScreen.java +++ b/src/com/efortin/frozenbubble/HomeScreen.java @@ -59,6 +59,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Typeface; import android.os.Bundle; import android.util.TypedValue; import android.view.KeyEvent; @@ -71,48 +72,110 @@ import android.widget.RelativeLayout; import android.widget.RelativeLayout.LayoutParams; -public class SplashScreen extends Activity { +public class HomeScreen extends Activity { /* * Provide unique IDs for the views associated with the relative * layout. These are used to define relative view layout positions * with respect to other views in the layout. - * + * * These IDs are generated automatically if using an XML layout, but * this object implements a RelativeLayout that is constructed purely * programmatically. */ private final static int SCREEN_ID = 100; - private final static int BTN1_ID = 101; - private final static int BTN2_ID = 102; - private final static int BTN3_ID = 103; + private final static int BACK_ID = 101; + private final static int BTN1_ID = 102; + private final static int BTN2_ID = 103; + private final static int BTN3_ID = 104; + private final static int BTN4_ID = 105; + private final static int BTN5_ID = 106; + private final static int BTN6_ID = 107; + private final static int BTN7_ID = 108; + private final static int BTN8_ID = 109; private static int buttonSelected = BTN1_ID; + private static int buttonSelPage1 = BTN1_ID; + private static int buttonSelPage2 = BTN4_ID; + private static int buttonSelPage3 = BTN7_ID; - private Boolean homeShown = false; - private Boolean musicOn = true; - private ImageView myImageView = null; + private boolean finished = false; + private boolean homeShown = false; + private boolean musicOn = true; + private ImageView myImageView = null; private RelativeLayout myLayout = null; - private ModPlayer myModPlayer = null; - private Thread splashThread = null; + private ModPlayer myModPlayer = null; + private Thread splashThread = null; + + /** + * Given that we are using a relative layout for the home screen in + * order to display the background image and various buttons, this + * function programmatically adds an on-screen back button to the + * layout. + */ + private void addBackButton() { + /* + * Construct the back button. + */ + Button backButton = new Button(this); + backButton.setOnClickListener(new Button.OnClickListener(){ + public void onClick(View v){ + backKeyPress(); + } + }); + backButton.setOnTouchListener(new Button.OnTouchListener(){ + public boolean onTouch(View v, MotionEvent event){ + if (event.getAction() == MotionEvent.ACTION_DOWN) + v.requestFocus(); + return false; + } + }); + /* + * Set the back button text to the following Unicode character: + * Anticlockwise Top Semicircle Arrow + * http://en.wikipedia.org/wiki/Arrow_(symbol) + */ + backButton.setText("\u21B6"); + backButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 30); + backButton.setWidth((int) backButton.getTextSize() * 2); + backButton.setTypeface(null, Typeface.BOLD); + backButton.setBackgroundResource(R.drawable.round_button); + backButton.setId(BACK_ID); + backButton.setFocusable(true); + backButton.setFocusableInTouchMode(true); + LayoutParams myParams = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + myParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + myParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + myParams.topMargin = 25; + myParams.rightMargin = 25; + /* + * Add view to layout. + */ + myLayout.addView(backButton, myParams); + } /** * Given that we are using a relative layout for the home screen in * order to display the background image and various buttons, this * function adds the buttons to the layout to provide game options to * the player. - *

- * The buttons are defined in relation to one another so that when + *

The buttons are defined in relation to one another so that when * using keys to navigate the buttons, the appropriate button will be * highlighted. */ private void addHomeButtons() { - // Construct the 2 player game button. + /* + * Construct the 2 player game button. + */ Button start2pGameButton = new Button(this); start2pGameButton.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v){ buttonSelected = BTN2_ID; - // Process the button tap and start/resume a 2 player game. - startFrozenBubble(2); + buttonSelPage1 = BTN2_ID; + /* + * Display the 2 player mode buttons page. + */ + displayButtonPage(2); } }); start2pGameButton.setOnTouchListener(new Button.OnTouchListener(){ @@ -122,9 +185,10 @@ public boolean onTouch(View v, MotionEvent event){ return false; } }); - start2pGameButton.setText("Player vs. CPU"); - start2pGameButton.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); - start2pGameButton.setWidth((int) (start2pGameButton.getTextSize() * 10)); + start2pGameButton.setText("2 Player"); + start2pGameButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + start2pGameButton.setWidth((int) (start2pGameButton.getTextSize() * 9)); + start2pGameButton.setTypeface(null, Typeface.BOLD); start2pGameButton.setHorizontalFadingEdgeEnabled(true); start2pGameButton.setFadingEdgeLength(5); start2pGameButton.setShadowLayer(5, 5, 5, R.color.black); @@ -133,19 +197,25 @@ public boolean onTouch(View v, MotionEvent event){ start2pGameButton.setFocusableInTouchMode(true); LayoutParams myParams1 = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - myParams1.addRule(RelativeLayout.CENTER_HORIZONTAL); - myParams1.addRule(RelativeLayout.CENTER_VERTICAL); + myParams1.addRule(RelativeLayout.CENTER_IN_PARENT); myParams1.topMargin = 15; myParams1.bottomMargin = 15; - // Add view to layout. + /* + * Add view to layout. + */ myLayout.addView(start2pGameButton, myParams1); - // Construct the 1 player game button. + /* + * Construct the 1 player game button. + */ Button start1pGameButton = new Button(this); start1pGameButton.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v){ buttonSelected = BTN1_ID; - // Process the button tap and start/resume a 1 player game. - startFrozenBubble(1); + buttonSelPage1 = BTN1_ID; + /* + * Process the button tap and start/resume a 1 player game. + */ + startFrozenBubble(VirtualInput.PLAYER1, 1, FrozenBubble.LOCALE_LOCAL); } }); start1pGameButton.setOnTouchListener(new Button.OnTouchListener(){ @@ -155,9 +225,10 @@ public boolean onTouch(View v, MotionEvent event){ return false; } }); - start1pGameButton.setText("Puzzle"); - start1pGameButton.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); - start1pGameButton.setWidth((int) (start1pGameButton.getTextSize() * 10)); + start1pGameButton.setText("1 Player"); + start1pGameButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + start1pGameButton.setWidth((int) (start1pGameButton.getTextSize() * 9)); + start1pGameButton.setTypeface(null, Typeface.BOLD); start1pGameButton.setHorizontalFadingEdgeEnabled(true); start1pGameButton.setFadingEdgeLength(5); start1pGameButton.setShadowLayer(5, 5, 5, R.color.black); @@ -166,18 +237,25 @@ public boolean onTouch(View v, MotionEvent event){ start1pGameButton.setFocusableInTouchMode(true); LayoutParams myParams2 = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - myParams2.addRule(RelativeLayout.CENTER_HORIZONTAL); + myParams2.addRule(RelativeLayout.CENTER_IN_PARENT); myParams2.addRule(RelativeLayout.ABOVE, start2pGameButton.getId()); myParams2.topMargin = 15; myParams2.bottomMargin = 15; - // Add view to layout. + /* + * Add view to layout. + */ myLayout.addView(start1pGameButton, myParams2); - // Construct the options button. + /* + * Construct the options button. + */ Button optionsButton = new Button(this); optionsButton.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v){ buttonSelected = BTN3_ID; - // Process the button tap and start the preferences activity. + buttonSelPage1 = BTN3_ID; + /* + * Process the button tap and start the preferences activity. + */ startPreferencesScreen(); } }); @@ -189,8 +267,9 @@ public boolean onTouch(View v, MotionEvent event){ } }); optionsButton.setText("Options"); - optionsButton.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); - optionsButton.setWidth((int) (optionsButton.getTextSize() * 10)); + optionsButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + optionsButton.setWidth((int) (optionsButton.getTextSize() * 9)); + optionsButton.setTypeface(null, Typeface.BOLD); optionsButton.setHorizontalFadingEdgeEnabled(true); optionsButton.setFadingEdgeLength(5); optionsButton.setShadowLayer(5, 5, 5, R.color.black); @@ -199,14 +278,281 @@ public boolean onTouch(View v, MotionEvent event){ optionsButton.setFocusableInTouchMode(true); LayoutParams myParams3 = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - myParams3.addRule(RelativeLayout.CENTER_HORIZONTAL); + myParams3.addRule(RelativeLayout.CENTER_IN_PARENT); myParams3.addRule(RelativeLayout.BELOW, start2pGameButton.getId()); myParams3.topMargin = 15; myParams3.bottomMargin = 15; - // Add view to layout. + /* + * Add view to layout. + */ myLayout.addView(optionsButton, myParams3); } + /** + * Given that we are using a relative layout for the home screen in + * order to display the background image and various buttons, this + * function adds the buttons to the layout to provide multiplayer game + * options to the player. + *

The buttons are defined in relation to one another so that when + * using keys to navigate the buttons, the appropriate button will be + * highlighted. + */ + private void addMultiplayerButtons() { + /* + * Construct the LAN game button. + */ + Button startLanGameButton = new Button(this); + startLanGameButton.setOnClickListener(new Button.OnClickListener(){ + public void onClick(View v){ + buttonSelected = BTN5_ID; + buttonSelPage2 = BTN5_ID; + /* + * Display the player ID buttons page. + */ + displayButtonPage(3); + } + }); + startLanGameButton.setOnTouchListener(new Button.OnTouchListener(){ + public boolean onTouch(View v, MotionEvent event){ + if (event.getAction() == MotionEvent.ACTION_DOWN) + v.requestFocus(); + return false; + } + }); + startLanGameButton.setText("Local Network"); + startLanGameButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + startLanGameButton.setWidth((int) (startLanGameButton.getTextSize() * 9)); + startLanGameButton.setTypeface(null, Typeface.BOLD); + startLanGameButton.setHorizontalFadingEdgeEnabled(true); + startLanGameButton.setFadingEdgeLength(5); + startLanGameButton.setShadowLayer(5, 5, 5, R.color.black); + startLanGameButton.setId(BTN5_ID); + startLanGameButton.setFocusable(true); + startLanGameButton.setFocusableInTouchMode(true); + LayoutParams myParams1 = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + myParams1.addRule(RelativeLayout.CENTER_IN_PARENT); + myParams1.topMargin = 15; + myParams1.bottomMargin = 15; + /* + * Add view to layout. + */ + myLayout.addView(startLanGameButton, myParams1); + /* + * Construct the Player vs. CPU game button. + */ + Button startCPUGameButton = new Button(this); + startCPUGameButton.setOnClickListener(new Button.OnClickListener(){ + public void onClick(View v){ + buttonSelected = BTN4_ID; + buttonSelPage2 = BTN4_ID; + /* + * Process the button tap and start a 2 player game. + */ + startFrozenBubble(VirtualInput.PLAYER1, 2, FrozenBubble.LOCALE_LOCAL); + } + }); + startCPUGameButton.setOnTouchListener(new Button.OnTouchListener(){ + public boolean onTouch(View v, MotionEvent event){ + if (event.getAction() == MotionEvent.ACTION_DOWN) + v.requestFocus(); + return false; + } + }); + startCPUGameButton.setText("Player vs. CPU"); + startCPUGameButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + startCPUGameButton.setWidth((int) (startCPUGameButton.getTextSize() * 9)); + startCPUGameButton.setTypeface(null, Typeface.BOLD); + startCPUGameButton.setHorizontalFadingEdgeEnabled(true); + startCPUGameButton.setFadingEdgeLength(5); + startCPUGameButton.setShadowLayer(5, 5, 5, R.color.black); + startCPUGameButton.setId(BTN4_ID); + startCPUGameButton.setFocusable(true); + startCPUGameButton.setFocusableInTouchMode(true); + LayoutParams myParams2 = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + myParams2.addRule(RelativeLayout.CENTER_IN_PARENT); + myParams2.addRule(RelativeLayout.ABOVE, startLanGameButton.getId()); + myParams2.topMargin = 15; + myParams2.bottomMargin = 15; + /* + * Add view to layout. + */ + myLayout.addView(startCPUGameButton, myParams2); + /* + * Construct the Internet game button. + */ + Button startIPGameButton = new Button(this); + startIPGameButton.setOnClickListener(new Button.OnClickListener(){ + public void onClick(View v){ + buttonSelected = BTN6_ID; + buttonSelPage2 = BTN6_ID; + /* + * Display the player ID buttons page. + */ + displayButtonPage(3); + } + }); + startIPGameButton.setOnTouchListener(new Button.OnTouchListener(){ + public boolean onTouch(View v, MotionEvent event){ + if (event.getAction() == MotionEvent.ACTION_DOWN) + v.requestFocus(); + return false; + } + }); + startIPGameButton.setText("Internet"); + startIPGameButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + startIPGameButton.setWidth((int) (startIPGameButton.getTextSize() * 9)); + startIPGameButton.setTypeface(null, Typeface.BOLD); + startIPGameButton.setHorizontalFadingEdgeEnabled(true); + startIPGameButton.setFadingEdgeLength(5); + startIPGameButton.setShadowLayer(5, 5, 5, R.color.black); + startIPGameButton.setId(BTN6_ID); + startIPGameButton.setFocusable(true); + startIPGameButton.setFocusableInTouchMode(true); + LayoutParams myParams3 = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + myParams3.addRule(RelativeLayout.CENTER_IN_PARENT); + myParams3.addRule(RelativeLayout.BELOW, startLanGameButton.getId()); + myParams3.topMargin = 15; + myParams3.bottomMargin = 15; + /* + * Add view to layout. + */ + myLayout.addView(startIPGameButton, myParams3); + } + + /** + * Given that we are using a relative layout for the home screen in + * order to display the background image and various buttons, this + * function adds the buttons to the layout to provide player ID + * selection options to the player. + *

The buttons are defined in relation to one another so that when + * using keys to navigate the buttons, the appropriate button will be + * highlighted. + */ + private void addPlayerSelectButtons() { + /* + * Construct the player 2 button. + */ + Button player2Button = new Button(this); + player2Button.setOnClickListener(new Button.OnClickListener(){ + public void onClick(View v){ + buttonSelected = BTN8_ID; + buttonSelPage3 = BTN8_ID; + /* + * Process the button tap and start a 2 player game. + */ + if (buttonSelPage2 == BTN6_ID) { + startFrozenBubble(VirtualInput.PLAYER2, 2, + FrozenBubble.LOCALE_INTERNET); + } + else { + startFrozenBubble(VirtualInput.PLAYER2, 2, FrozenBubble.LOCALE_LAN); + } + } + }); + player2Button.setOnTouchListener(new Button.OnTouchListener(){ + public boolean onTouch(View v, MotionEvent event){ + if (event.getAction() == MotionEvent.ACTION_DOWN) + v.requestFocus(); + return false; + } + }); + player2Button.setText("Player 2"); + player2Button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + player2Button.setWidth((int) (player2Button.getTextSize() * 9)); + player2Button.setTypeface(null, Typeface.BOLD); + player2Button.setHorizontalFadingEdgeEnabled(true); + player2Button.setFadingEdgeLength(5); + player2Button.setShadowLayer(5, 5, 5, R.color.black); + player2Button.setId(BTN8_ID); + player2Button.setFocusable(true); + player2Button.setFocusableInTouchMode(true); + LayoutParams myParams1 = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + myParams1.addRule(RelativeLayout.CENTER_IN_PARENT); + myParams1.topMargin = 15; + myParams1.bottomMargin = 15; + /* + * Add view to layout. + */ + myLayout.addView(player2Button, myParams1); + /* + * Construct the player 1 button. + */ + Button player1Button = new Button(this); + player1Button.setOnClickListener(new Button.OnClickListener(){ + public void onClick(View v){ + buttonSelected = BTN7_ID; + buttonSelPage3 = BTN7_ID; + /* + * Process the button tap and start a 2 player game. + */ + if (buttonSelPage2 == BTN6_ID) { + startFrozenBubble(VirtualInput.PLAYER1, 2, + FrozenBubble.LOCALE_INTERNET); + } + else { + startFrozenBubble(VirtualInput.PLAYER1, 2, FrozenBubble.LOCALE_LAN); + } + } + }); + player1Button.setOnTouchListener(new Button.OnTouchListener(){ + public boolean onTouch(View v, MotionEvent event){ + if (event.getAction() == MotionEvent.ACTION_DOWN) + v.requestFocus(); + return false; + } + }); + player1Button.setText("Player 1"); + player1Button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + player1Button.setWidth((int) (player1Button.getTextSize() * 9)); + player1Button.setTypeface(null, Typeface.BOLD); + player1Button.setHorizontalFadingEdgeEnabled(true); + player1Button.setFadingEdgeLength(5); + player1Button.setShadowLayer(5, 5, 5, R.color.black); + player1Button.setId(BTN7_ID); + player1Button.setFocusable(true); + player1Button.setFocusableInTouchMode(true); + LayoutParams myParams2 = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + myParams2.addRule(RelativeLayout.CENTER_IN_PARENT); + myParams2.addRule(RelativeLayout.ABOVE, player2Button.getId()); + myParams2.topMargin = 15; + myParams2.bottomMargin = 15; + /* + * Add view to layout. + */ + myLayout.addView(player1Button, myParams2); + } + + private void backKeyPress() { + /* + * When one of the multiplayer game buttons was selected, if the + * back button was pressed, remove the multiplayer buttons and + * display the home buttons. The 2 player button becomes selected + * by default on the home screen. + * + * Otherwise if one of the base level buttons was selected, then + * terminate the home screen activity. + */ + if ((buttonSelected == BTN4_ID) || + (buttonSelected == BTN5_ID) || + (buttonSelected == BTN6_ID)) { + displayButtonPage(1); + } + else if ((buttonSelected == BTN7_ID) || + (buttonSelected == BTN8_ID)) { + displayButtonPage(2); + } + else { + finished = true; + cleanUp(); + finish(); + } + } + private void cleanUp() { if (myModPlayer != null) { myModPlayer.destroyMusicPlayer(); @@ -214,15 +560,51 @@ private void cleanUp() { } } + /** + * Manage a set of button "pages", where each page displays buttons. + * The pages are indexed by a unique identifier. When a valid page + * identifier is provided, all buttons corresponding to other pages + * are removed and the buttons for the requested page ID are added. + * @param pageID - the requested page identifier (1-based). + */ + private void displayButtonPage(int pageID) { + if (pageID == 1) { + buttonSelected = buttonSelPage1; + removeViewByID(BTN4_ID); + removeViewByID(BTN5_ID); + removeViewByID(BTN6_ID); + removeViewByID(BTN7_ID); + removeViewByID(BTN8_ID); + addHomeButtons(); + selectInitialButton(); + } + else if (pageID == 2) { + buttonSelected = buttonSelPage2; + removeViewByID(BTN1_ID); + removeViewByID(BTN2_ID); + removeViewByID(BTN3_ID); + removeViewByID(BTN7_ID); + removeViewByID(BTN8_ID); + addMultiplayerButtons(); + selectInitialButton(); + } + else if (pageID == 3) { + buttonSelected = buttonSelPage3; + removeViewByID(BTN1_ID); + removeViewByID(BTN2_ID); + removeViewByID(BTN3_ID); + removeViewByID(BTN4_ID); + removeViewByID(BTN5_ID); + removeViewByID(BTN6_ID); + addPlayerSelectButtons(); + selectInitialButton(); + } + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - cleanUp(); - // - // Terminate the splash screen activity. - // - // - finish(); + backKeyPress(); return true; } return super.onKeyDown(keyCode, event); @@ -231,14 +613,16 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { /* * (non-Javadoc) * @see android.app.Activity#onCreate(android.os.Bundle) - * * Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + finished = false; restoreGamePrefs(); - // Configure the window presentation and layout. + /* + * Configure the window presentation and layout. + */ setWindowLayout(); myLayout = new RelativeLayout(this); myLayout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, @@ -246,7 +630,9 @@ public void onCreate(Bundle savedInstanceState) { myImageView = new ImageView(this); if (FrozenBubble.numPlayers != 0) - startFrozenBubble(FrozenBubble.numPlayers); + startFrozenBubble(FrozenBubble.myPlayerId, + FrozenBubble.numPlayers, + FrozenBubble.gameLocale); else if (getIntent().hasExtra("startHomeScreen")) { setBackgroundImage(R.drawable.home_screen); setContentView(myLayout); @@ -255,35 +641,38 @@ else if (getIntent().hasExtra("startHomeScreen")) { else { setBackgroundImage(R.drawable.splash); setContentView(myLayout); - // - // Thread for managing the splash screen. - // - // + /* + * Thread for managing the splash screen. + */ splashThread = new Thread() { @Override public void run() { try { synchronized(this) { - // - // TODO: The splash screen waits before launching the - // game activity. Change this so that the game - // activity is started immediately, and notifies - // the splash screen activity when it is done - // loading saved state data and preferences, so the - // splash screen functions as a distraction from - // game loading latency. There is no advantage in - // doing this right now, because there is no lag. - // - // - wait(3000); // wait 3 seconds + /* + * TODO: The splash screen waits before launching the + * game activity. Change this so that the game activity + * is started immediately, and notifies the splash screen + * activity when it is done loading saved state data and + * preferences, so the splash screen functions as a + * distraction from game loading latency. There is no + * advantage in doing this right now, because there is no + * perceivable lag. + */ + /* + * Display the splash screen image for 3 seconds. + */ + wait(3000); } } catch (InterruptedException e) { } finally { - runOnUiThread(new Runnable() { - public void run() { - startHomeScreen(); - } - }); + if (!finished) { + runOnUiThread(new Runnable() { + public void run() { + startHomeScreen(); + } + }); + } } } }; @@ -312,7 +701,6 @@ public void onResume() { /* * (non-Javadoc) * @see android.app.Activity#onTouchEvent(android.view.MotionEvent) - * * Invoked when the screen is touched. */ @Override @@ -327,6 +715,12 @@ public boolean onTouchEvent(MotionEvent event) { return true; } + private void removeViewByID(int id) { + if (myLayout != null) { + myLayout.removeView(myLayout.findViewById(id)); + } + } + private void restoreGamePrefs() { SharedPreferences mConfig = getSharedPreferences(FrozenBubble.PREFS_NAME, Context.MODE_PRIVATE); @@ -334,7 +728,9 @@ private void restoreGamePrefs() { } private void selectInitialButton() { - // Select the last button that was pressed. + /* + * Select the last button that was pressed. + */ Button selectedButton = (Button) myLayout.findViewById(buttonSelected); selectedButton.requestFocus(); selectedButton.setSelected(true); @@ -354,7 +750,6 @@ private void setBackgroundImage(int resId) { /** * Set the window layout according to the game preferences. - * *

Requesting that the title bar be removed must be * performed before setting the view content by applying the XML * layout, or it will generate an exception. @@ -362,9 +757,13 @@ private void setBackgroundImage(int resId) { private void setWindowLayout() { final int flagFs = WindowManager.LayoutParams.FLAG_FULLSCREEN; final int flagNoFs = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; - // Remove the title bar. + /* + * Remove the title bar. + */ requestWindowFeature(Window.FEATURE_NO_TITLE); - // Set full screen mode based on the game preferences. + /* + * Set full screen mode based on the game preferences. + */ SharedPreferences mConfig = getSharedPreferences(FrozenBubble.PREFS_NAME, Context.MODE_PRIVATE); boolean fullscreen = mConfig.getBoolean("fullscreen", true); @@ -379,24 +778,36 @@ private void setWindowLayout() { } } - private void startFrozenBubble(int numPlayers) { - // - // Since the default game activity creates its own player, - // destroy the current player. - // - // + /** + * Start the game with the specified number of players in the + * specified locale. A 1 player game can only be played locally. + * @param myPlayerId - the local player ID. + * @param numPlayers - the number of players (1 or 2) + * @param gameLocale - the location of the opponent. A local opponent + * will be played by the CPU. A LAN opponent will be played over the + * network using multicasting, and an internet opponent will be played + * using TCP. + */ + private void startFrozenBubble(int myPlayerId, + int numPlayers, + int gameLocale) { + finished = true; + /* + * Since the default game activity creates its own player, + * destroy the current player. + */ cleanUp(); - // - // Create an intent to launch the activity to play the game. - // - // + /* + * Create an intent to launch the activity to play the game. + */ Intent intent = new Intent(this, FrozenBubble.class); + intent.putExtra("myPlayerId", (int)myPlayerId); intent.putExtra("numPlayers", (int)numPlayers); + intent.putExtra("gameLocale", (int)gameLocale); startActivity(intent); - // - // Terminate the splash screen activity. - // - // + /* + * Terminate the splash screen activity. + */ finish(); } @@ -404,14 +815,28 @@ private void startHomeScreen() { if (!homeShown) { homeShown = true; setBackgroundImage(R.drawable.home_screen); - addHomeButtons(); + addBackButton(); + if ((buttonSelected == BTN1_ID) || + (buttonSelected == BTN2_ID) || + (buttonSelected == BTN3_ID)) + addHomeButtons(); + else if ((buttonSelected == BTN4_ID) || + (buttonSelected == BTN5_ID) || + (buttonSelected == BTN6_ID)) + addMultiplayerButtons(); + else + addPlayerSelectButtons(); setContentView(myLayout); myLayout.setFocusable(true); myLayout.setFocusableInTouchMode(true); myLayout.requestFocus(); - // Highlight the appropriate button to show as selected. + /* + * Highlight the appropriate button to show as selected. + */ selectInitialButton(); - // Create a new music player to play the home screen music. + /* + * Create a new music player to play the home screen music. + */ myModPlayer = new ModPlayer(this, R.raw.introzik, musicOn, false); } } diff --git a/src/com/efortin/frozenbubble/ModPlayer.java b/src/com/efortin/frozenbubble/ModPlayer.java index 9c74912..9f88545 100644 --- a/src/com/efortin/frozenbubble/ModPlayer.java +++ b/src/com/efortin/frozenbubble/ModPlayer.java @@ -81,13 +81,10 @@ public void destroyMusicPlayer() { /** * Load a new song. - * - * @param songId - * - The song resource ID. - * - * @param startPlaying - * - If true, the song starts playing immediately. Otherwise - * it is paused and must be unpaused to start playing. + * @param songId - The song resource ID. + * @param startPlaying - If true, the song starts playing + * immediately. Otherwise it is paused and must be unpaused to start + * playing. */ public void loadNewSong(int songId, boolean startPlaying) { if (resplayer != null) { @@ -102,16 +99,11 @@ public void loadNewSong(int songId, boolean startPlaying) { /** * Create a new music player. - * - * @param context - * - The application context. - * - * @param songId - * - The song resource ID. - * - * @param startPaused - * - If false, the song starts playing immediately. Otherwise - * it is paused and must be unpaused to start playing. + * @param context - The application context. + * @param songId - The song resource ID. + * @param startPaused - If false, the song starts playing + * immediately. Otherwise it is paused and must be unpaused to start + * playing. */ private void newMusicPlayer(Context context, int songId, diff --git a/src/com/efortin/frozenbubble/MulticastManager.java b/src/com/efortin/frozenbubble/MulticastManager.java new file mode 100644 index 0000000..45a3866 --- /dev/null +++ b/src/com/efortin/frozenbubble/MulticastManager.java @@ -0,0 +1,575 @@ +/* + * [[ Frozen-Bubble ]] + * + * Copyright (c) 2000-2003 Guillaume Cottenceau. + * Java sourcecode - Copyright (c) 2003 Glenn Sanson. + * Additional source - Copyright (c) 2013 Eric Fortin. + * + * This code is distributed under the GNU General Public License + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 or 3, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to: + * Free Software Foundation, Inc. + * 675 Mass Ave + * Cambridge, MA 02139, USA + * + * Artwork: + * Alexis Younes <73lab at free.fr> + * (everything but the bubbles) + * Amaury Amblard-Ladurantie + * (the bubbles) + * + * Soundtrack: + * Matthias Le Bidan + * (the three musics and all the sound effects) + * + * Design & Programming: + * Guillaume Cottenceau + * (design and manage the project, whole Perl sourcecode) + * + * Java version: + * Glenn Sanson + * (whole Java sourcecode, including JIGA classes + * http://glenn.sanson.free.fr/jiga/) + * + * Android port: + * Pawel Aleksander Fedorynski + * Eric Fortin + * Copyright (c) Google Inc. + * + * [[ http://glenn.sanson.free.fr/fb/ ]] + * [[ http://www.frozen-bubble.org/ ]] + */ + +package com.efortin.frozenbubble; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.UnknownHostException; +import java.util.ArrayList; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.text.format.Formatter; +import android.util.Log; + +/** + * Multicast manager class - implements UDP unicast and multicast + * datagram sending and receiving. This implementation currently only + * supports IPv4 internet addresses. + *

This class instantiates a thread to send and receive WiFi UDP + * unicast or multicast messages, depending on the constructor utilized. + * The MulticastSocket class is a descendant of + * DatagramSocket, the UDP datagram socket class. Thus a + * multicast socket can be used to send and receive either UDP unicast + * datagrams, or UDP multicast datagrams, which makes it convenient to + * use either peer to peer networking scheme using virtually identical + * methods. + *

Multicast host addresses must be in the IPv4 class D address + * range, with the first octet being within the 224 to 239 range. + * For example, "225.0.0.15" is an actual IPv4 multicast + * address. + *

Refer to: + * http://en.wikipedia.org/wiki/Multicast_address for + * information regarding Multicast address usage and restrictions. + *

A typical UDP multicast implementation looks like this: + *


+ * MulticastManager session =
+ *     new MulticastManager(context, addr, port);
+ * session.setMulticastListener(this);
+ * 
+ * or alternatively the UDP unicast implementation appears as follows: + *

+ * MulticastManager session =
+ *     new MulticastManager(context, hostName, port);
+ * session.setMulticastListener(this);
+ * 
+ *

The context from which to obtain the application context, the IP + * address or host name of the UDP peer, and the port of the UDP socket + * session must be supplied when creating a new + * MulticastManager instance. + *

The following uses permissions must be added to the + * Android project manifest to access all the API functionality required + * to perform UDP unicast or multicast networking as implemented:
+ * ACCESS_NETWORK_STATE
+ * ACCESS_WIFI_STATE
+ * CHANGE_WIFI_MULTICAST_STATE
+ * INTERNET
+ * @author Eric Fortin, Wednesday, May 8, 2013 + */ +public class MulticastManager { + private static final String LOG_TAG = MulticastManager.class.getSimpleName(); + + /* + * The following value turns of the multicast receive filter. + */ + public static final byte FILTER_OFF = -1; + + /* + * Multicast network transport layer event enumeration. + */ + public static enum eventEnum { + PACKET_RX, + RX_FAIL, + TX_FAIL; + } + + /* + * Listener interface for various multicast management events. + * + * This interface defines the abstract listener method that needs + * to be instantiated by the registrar, as well as the various + * events supported by the interface. + */ + public interface MulticastListener { + public abstract void onMulticastEvent(eventEnum event, + byte[] buffer, + int length); + } + + public void setMulticastListener(MulticastListener ml) { + mListener = ml; + } + + /* + * MulticastManager class member variables. + */ + private boolean paused; + private boolean running; + private byte filter; + private int mPort; + private ArrayList txList = null; + private Context mContext = null; + private InetAddress mAddress = null; + private MulticastListener mListener = null; + private MulticastSocket mSocket = null; + private Thread mThread = null; + private WifiManager.MulticastLock mLock = null; + + /** + * Multicast manager UDP multicast class constructor. + *

When created, this class instantiates a thread to send and + * receive WiFi UDP multicast messages. + *

Multicast host addresses must be in the IPv4 class D address + * range, with the first octet being within the 224 to 239 range. + * For example, "225.0.0.15" is an actual IPv4 multicast + * address. + *

Refer to: + * http://en.wikipedia.org/wiki/Multicast_address + * for information regarding Multicast address usage and restrictions. + *

A typical implementation looks like this: + *


+   * MulticastManager session =
+   *     new MulticastManager(context, addr, port);
+   * session.setMulticastListener(this);
+   * 
+ *

The context from which to obtain the application context, the IP + * address of the UDP multicast session, and the port of the multicast + * session must be supplied when creating a new + * MulticastManager instance. + *

The following uses permissions must be added to the + * Android project manifest to perform multicast networking:
+ * CHANGE_WIFI_MULTICAST_STATE
+ * INTERNET
+ * @param context - the context from which to obtain the application + * context for the purpose of obtaining WiFi service access. + * @param address - the internet address of this multicast session. + * @param port - the port number to use for the multicast socket. + */ + public MulticastManager(Context context, + byte[]address, + int port) throws UnknownHostException { + mContext = context.getApplicationContext(); + mPort = port; + filter = FILTER_OFF; + mListener = null; + mLock = null; + mThread = null; + txList = null; + if (configureMulticast(address, port) == null) { + throw new UnknownHostException(); + } + txList = new ArrayList(); + WifiManager wm = + (WifiManager)mContext.getSystemService(Context.WIFI_SERVICE); + mLock = wm.createMulticastLock("multicastLock"); + mLock.setReferenceCounted(true); + mLock.acquire(); + mThread = new Thread(new MulticastThread(), "mThread"); + mThread.start(); + } + + /** + * Multicast manager UDP unicast class constructor. + *

When created, this class instantiates a thread to send and + * receive WiFi UDP unicast messages. + *

A typical implementation looks like this: + *


+   * MulticastManager session =
+   *     new MulticastManager(context, hostName, port);
+   * session.setMulticastListener(this);
+   * 
+ *

The context from which to obtain the application context, the + * host name of the UDP unicast peer to connect to, and the port of + * the UDP unicast session must be supplied when creating a new + * MulticastManager instance. + *

The following uses permissions must be addeded to + * the Android project manifest to perform UDP networking:
+ * INTERNET + * @param context - the context from which to obtain the application + * context for the purpose of obtaining WiFi service access. + * @param hostName - the host name of the UDP unicast peer. + * @param port - the port number to use for the UDP socket. + */ + public MulticastManager(Context context, + String hostName, + int port) throws UnknownHostException { + mContext = context.getApplicationContext(); + mPort = port; + filter = FILTER_OFF; + mListener = null; + mLock = null; + mThread = null; + txList = null; + if (configureMulticast(hostName, port) == null) { + throw new UnknownHostException(); + } + txList = new ArrayList(); + mThread = new Thread(new MulticastThread(), "mThread"); + mThread.start(); + } + + /** + * Clean up the multicast manager by stopping the thread, closing the + * multicast socket and freeing resources. + */ + public void cleanUp() { + mListener = null; + stopThread(); + if (mSocket != null) { + mSocket.close(); + } + mSocket = null; + if (txList != null) { + txList.clear(); + } + txList = null; + if (mLock != null) { + mLock.release(); + } + mLock = null; + } + + /** + * Configure the multicast socket settings. + *

This must be called before start()ing the thread. + * @param address - the internet address of this multicast session. + * @param port - the port number to use for the multicast socket. + */ + private MulticastSocket configureMulticast(byte[]address, int port) { + try { + mAddress = InetAddress.getByAddress(address); + mSocket = new MulticastSocket(port); + mSocket.setSoTimeout(101); + mSocket.setBroadcast(false); + mSocket.setLoopbackMode(true); + mSocket.joinGroup(mAddress); + } catch (UnknownHostException uhe) { + mSocket = null; + uhe.printStackTrace(); + } catch (IOException ioe) { + mSocket = null; + ioe.printStackTrace(); + } + return mSocket; + } + + /** + * Configure the multicast socket settings. + *

This must be called before start()ing the thread. + * @param hostName - the host name of the UDP unicast peer. + * @param port - the port number to use for the UDP socket. + */ + private MulticastSocket configureMulticast(String hostName, int port) { + try { + mAddress = InetAddress.getByName(hostName); + mSocket = new MulticastSocket(port); + mSocket.setSoTimeout(101); + mSocket.setBroadcast(false); + mSocket.setLoopbackMode(true); + } catch (UnknownHostException uhe) { + mSocket = null; + uhe.printStackTrace(); + } catch (IOException ioe) { + mSocket = null; + ioe.printStackTrace(); + } + return mSocket; + } + + /** + * Obtain the local IP address from the WiFiManager. + *

The following uses permission must be addeded to + * the Android project manifest to obtain the network connection + * status:
+ * ACCESS_WIFI_STATE + * @return the local WiFi IP address. + * @see WifiManager + */ + public String getLocalIpAddress() { + WifiManager wifiManager = + (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + return Formatter.formatIpAddress(wifiInfo.getIpAddress()); + } + + /** + * Check with the ConnectivityManager if the device is + * connected to the internet. + *

The following uses permission must be addeded to + * the Android project manifest to obtain the network connection + * status:
+ * ACCESS_NETWORK_STATE + * @return true if the device is connected to the + * internet. + * @see ConnectivityManager + */ + public boolean hasInternetConnection() + { + ConnectivityManager cm = + (ConnectivityManager) mContext.getSystemService(Context. + CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + if ((activeNetwork != null) && activeNetwork.isConnected()) + { + return true; + } + return false; + } + + /** + * This is the multicast thread declaration. + *

To support being able to send and receive packets in the same + * thread, a nonzero socket read timeout must be set, because + * MulticastSocket.receive() blocks until a packet is + * received or the socket times out. Thus, if a timeout of zero is + * set (which is the default, and denotes that the socket will never + * time out), a datagram will never be sent unless one has just been + * received. + * @author Eric Fortin, Wednesday, May 8, 2013 + * @see configureMulticast() + */ + private class MulticastThread implements Runnable { + private byte[] rxBuffer = new byte[256]; + + /** + * Receive a multicast datagram. + *

Given a nonzero socket timeout, it is expected behavior for + * this method to catch an InterruptedIOException. + * This method posts an EVENT_PACKET_RX event to the + * registered listener upon datagram receipt. + */ + private void receiveDatagram() { + if (!paused && running) try { + DatagramPacket dpRX = + new DatagramPacket(rxBuffer, rxBuffer.length, mAddress, mPort); + mSocket.receive(dpRX); + byte[] buffer = dpRX.getData(); + int length = dpRX.getLength(); + + if (!paused && running && (length != 0) && (mListener != null)) { + if ((filter == FILTER_OFF) || (filter == buffer[0])) { + mListener.onMulticastEvent(eventEnum.PACKET_RX, buffer, length); + Log.d(LOG_TAG, "received "+length+" bytes"); + } + } + } catch (NullPointerException npe) { + npe.printStackTrace(); + if (mListener != null) { + mListener.onMulticastEvent(eventEnum.RX_FAIL, null, 0); + } + } catch (InterruptedIOException iioe) { + /* + * Receive timeout. This is expected behavior. + */ + } catch (IOException ioe) { + ioe.printStackTrace(); + if (mListener != null) { + mListener.onMulticastEvent(eventEnum.RX_FAIL, null, 0); + } + } + } + + /** + * This is the thread's run() call. + *

Send multicast UDP messages, and read multicast datagrams from + * other clients. + *

To support being able to send and receive packets in the same + * thread, a nonzero socket read timeout must be set, because + * MulticastSocket.receive() blocks until a packet is + * received or the socket times out. Thus, if a timeout of zero is + * set (which is the default, and denotes that the socket will never + * time out), a datagram will never be sent unless one has just been + * received. + *

Thus the maximum time between datagram transmissions is the + * socket timeout if no datagrams are being recieved. If messages + * are being received, available TX throughput will be increased. + */ + @Override + public void run() { + paused = false; + running = true; + + while (running) { + if (paused) try { + synchronized(this) { + wait(); + } + } catch (InterruptedException ie) { + /* + * Interrupted. This is expected behavior. + */ + } + + if (!paused && running) { + sendDatagram(); + receiveDatagram(); + } + } + } + + /** + * Extract the next buffer from the FIFO transmit list and send it + * as a multicast datagram packet. + */ + private void sendDatagram() { + if (!paused && running && txList.size() > 0) try { + byte[] bytes; + synchronized(txList) { + bytes = txList.get(0); + } + mSocket.send(new DatagramPacket(bytes, bytes.length, mAddress, mPort)); + Log.d(LOG_TAG, "transmitted "+bytes.length+" bytes"); + synchronized(txList) { + txList.remove(0); + } + } catch (NullPointerException npe) { + npe.printStackTrace(); + if (mListener != null) { + mListener.onMulticastEvent(eventEnum.TX_FAIL, null, 0); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + if (mListener != null) { + mListener.onMulticastEvent(eventEnum.TX_FAIL, null, 0); + } + } + } + } + + public void pause() { + if (running) { + paused = true; + mSocket.disconnect(); + if (mLock != null) { + try { + mSocket.leaveGroup(mAddress); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + mLock.release(); + } + } + } + + /** + * Set the software datagram receive filter value. If the filter + * value is FILTER_OFF, then received messages are all passed to the + * registered listener(s). Otherwise, all messages that don't have + * the same value as the filter in the first byte of their datagram + * payload will be discarded. + * @param newFilter - the new recieve filter value to use. + */ + public void setFilter(byte newFilter) { + filter = newFilter; + } + + /** + * Stop and join() the multicast thread. + */ + private void stopThread() { + paused = false; + running = false; + if (mThread != null) { + synchronized(mThread) { + mThread.interrupt(); + } + } + /* + * Close and join() the multicast thread. + */ + boolean retry = true; + while (retry && (mThread != null)) { + try { + mThread.join(); + retry = false; + } catch (InterruptedException e) { + /* + * Keep trying to close the multicast thread. + */ + } + } + } + + /** + * Send the desired byte buffer as a multicast datagram packet. + * @param buffer - the byte buffer to transmit. + * @return true if the buffer was successfully added to + * the outgoing datagram transmit list, false if the the + * buffer was unable to be added to the transmit list. + */ + public boolean transmit(byte[] buffer) { + if ((mThread != null) && running) { + synchronized(txList) { + txList.add(buffer); + } + return true; + } + return false; + } + + public void unPause() { + paused = false; + try { + if (mLock != null) { + mLock.acquire(); + mSocket.joinGroup(mAddress); + } + mSocket.bind (new InetSocketAddress(mAddress, mPort)); + mSocket.connect(new InetSocketAddress(mAddress, mPort)); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + if (mThread != null) { + synchronized(mThread) { + mThread.interrupt(); + } + } + } +} diff --git a/src/com/efortin/frozenbubble/NetworkGameManager.java b/src/com/efortin/frozenbubble/NetworkGameManager.java new file mode 100644 index 0000000..50b4117 --- /dev/null +++ b/src/com/efortin/frozenbubble/NetworkGameManager.java @@ -0,0 +1,1809 @@ +/* + * [[ Frozen-Bubble ]] + * + * Copyright (c) 2000-2003 Guillaume Cottenceau. + * Java sourcecode - Copyright (c) 2003 Glenn Sanson. + * Additional source - Copyright (c) 2013 Eric Fortin. + * + * This code is distributed under the GNU General Public License + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 or 3, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to: + * Free Software Foundation, Inc. + * 675 Mass Ave + * Cambridge, MA 02139, USA + * + * Artwork: + * Alexis Younes <73lab at free.fr> + * (everything but the bubbles) + * Amaury Amblard-Ladurantie + * (the bubbles) + * + * Soundtrack: + * Matthias Le Bidan + * (the three musics and all the sound effects) + * + * Design & Programming: + * Guillaume Cottenceau + * (design and manage the project, whole Perl sourcecode) + * + * Java version: + * Glenn Sanson + * (whole Java sourcecode, including JIGA classes + * http://glenn.sanson.free.fr/jiga/) + * + * Android port: + * Pawel Aleksander Fedorynski + * Eric Fortin + * Copyright (c) Google Inc. + * + * [[ http://glenn.sanson.free.fr/fb/ ]] + * [[ http://www.frozen-bubble.org/ ]] + */ + +package com.efortin.frozenbubble; + +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import org.jfedor.frozenbubble.BubbleSprite; +import org.jfedor.frozenbubble.FrozenBubble; +import org.jfedor.frozenbubble.FrozenGame; +import org.jfedor.frozenbubble.MultiplayerGameView.NetGameInterface; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import com.efortin.frozenbubble.MulticastManager.MulticastListener; +import com.efortin.frozenbubble.MulticastManager.eventEnum; + +/** + * This class manages the actions in a network multiplayer game by + * sending the local actions to the remote player, and queueing the + * incoming remote player actions for enactment on the local machine. + *

The thread created by this class will not run() until + * newGame() is called. + *

Attach VirtualInput objects to this manager for each + * player in the network game. + * @author Eric Fortin + * + */ +public class NetworkGameManager extends Thread + implements MulticastListener, NetGameInterface { + private static final String MCAST_HOST_NAME = "225.0.0.15"; + private static final byte[] MCAST_BYTE_ADDR = { (byte) 225, 0, 0, 15 }; + private static final int PORT = 5500; + + /* + * Message identifier definitions. + */ + public static final byte MSG_ID_STATUS = 1; + public static final byte MSG_ID_PREFS = 2; + public static final byte MSG_ID_ACTION = 3; + public static final byte MSG_ID_FIELD = 4; + + /* + * Datagram size definitions. + */ + public static final int ACTION_BYTES = 37; + public static final int FIELD_BYTES = 112; + public static final int PREFS_BYTES = Preferences.PREFS_BYTES; + public static final int STATUS_BYTES = 13; + + /* + * Network game management definitions. + */ + private static final long ACTION_TIMEOUT = 521L; + private static final long GAME_START_TIMEOUT = 509L; + private static final long STATUS_TIMEOUT = 503L; + private static final byte PROTOCOL_VERSION = 1; + private static final byte GAME_ID_MAX = 100; + + /* + * UDP unicast and multicast connection type enumeration. + */ + public static enum connectEnum { + UDP_UNICAST, + UDP_MULTICAST; + } + + private byte myGameID; + private boolean anyStatusRx; + private boolean gotFieldData; + private boolean gotPrefsData; + private boolean[] gamesInProgress; + private boolean missedAction; + private boolean paused; + private boolean running; + private long actionTxTime; + private long gameStartTime; + private long statusTxTime; + private connectEnum connectType; + private String localIpAddress = null; + private String remoteIpAddress = null; + private Context myContext = null; + private PlayerStatus localStatus = null; + private PlayerStatus remoteStatus = null; + private Preferences localPrefs = null; + private Preferences remotePrefs = null; + private VirtualInput localPlayer = null; + private VirtualInput remotePlayer = null; + private GameFieldData remoteGameFieldData = null; + private PlayerAction remotePlayerAction = null; + private RemoteInterface remoteInterface = null; + private MulticastManager session = null; + + /* + * Keep action lists for action retransmission requests and game + * access. + */ + private ArrayList localActionList = null; + private ArrayList remoteActionList = null; + + /** + * Class constructor. + * @param myContext - the context from which to obtain the application + * context to pass to the transport layer. + * @param connectType - the transport layer connect type. + * @param localPlayer - reference to the local player input object. + * @param remotePlayer - reference to the remote player input object. + * transport layer to create a socket connection. + */ + public NetworkGameManager(Context myContext, + connectEnum connectType, + VirtualInput localPlayer, + VirtualInput remotePlayer) { + this.myContext = myContext.getApplicationContext(); + this.connectType = connectType; + this.localPlayer = localPlayer; + this.remotePlayer = remotePlayer; + /* + * The game ID is used as the transport layer receive filter. Do + * not filter messages until we have obtained a game ID. + */ + myGameID = MulticastManager.FILTER_OFF; + anyStatusRx = false; + gotFieldData = false; + gotPrefsData = false; + gamesInProgress = new boolean[GAME_ID_MAX]; + missedAction = false; + localIpAddress = null; + remoteIpAddress = null; + localPrefs = new Preferences(); + remotePrefs = new Preferences(); + localStatus = null; + remoteStatus = null; + remoteGameFieldData = new GameFieldData(null); + remotePlayerAction = new PlayerAction(null); + remoteInterface = new RemoteInterface(remotePlayerAction, + remoteGameFieldData); + session = null; + SharedPreferences sp = + myContext.getSharedPreferences(FrozenBubble.PREFS_NAME, + Context.MODE_PRIVATE); + PreferencesActivity.getFrozenBubblePrefs(localPrefs, sp); + /* + * Create the player action arrays. The actions are inserted + * chronologically based on message receipt order, but are extracted + * based on consecutive action ID. + */ + localActionList = new ArrayList(); + remoteActionList = new ArrayList(); + /* + * Set the preference request flag to request the game option data + * from the remote player. If this player is player 1, then don't + * request the preference data, since player 1's preferences are + * used as the game preferences for all players. + */ + boolean requestPrefs; + if (localPlayer.playerID == VirtualInput.PLAYER1) { + requestPrefs = false; + } + else { + requestPrefs = true; + } + /* + * Initialize the local status local action ID to zero, as it is + * pre-incremented for every action transmitted to the remote + * player. + * + * Initialize the local status remote action ID to 1, as it must be + * the first action ID received from the remote player. + */ + localStatus = new PlayerStatus((byte) localPlayer.playerID, + (short) 0, (short) 1, + false, false, requestPrefs, + (short) 0, (short) 0); + } + + /** + * This class represents the current state of an individual player + * game field. The game field consists of the launcher bubbles, the + * bubbles fixed to the game field, and the the attack bar. + * @author Eric Fortin + * + */ + public class GameFieldData { + public byte playerID = 0; + public short localActionID = 0; + public byte compressorSteps = 0; + public byte launchBubbleColor = -1; + public byte nextBubbleColor = -1; + public short attackBarBubbles = 0; + /* + * The game field is represented by a 2-dimensional array, with 8 + * rows and 13 columns. This is displayed on the screen as 13 rows + * with 8 columns. + */ + public byte[][] gameField = + {{ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }}; + + /** + * Class constructor. + * @param action - GameFieldData object to copy to this instance. + */ + public GameFieldData(GameFieldData fieldData) { + copyFromFieldData(fieldData); + } + + /** + * Class constructor. + * @param buffer - buffer contents to copy to this instance. + */ + public GameFieldData(byte[] buffer, int startIndex) { + copyFromBuffer(buffer, startIndex); + } + + /** + * Copy the contents of the supplied field data to this field data. + * @param action - the action to copy + */ + public void copyFromFieldData(GameFieldData fieldData) { + if (fieldData != null) { + this.playerID = fieldData.playerID; + this.localActionID = fieldData.localActionID; + this.compressorSteps = fieldData.compressorSteps; + this.launchBubbleColor = fieldData.launchBubbleColor; + this.nextBubbleColor = fieldData.nextBubbleColor; + this.attackBarBubbles = fieldData.attackBarBubbles; + + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 13; y++) { + this.gameField[x][y] = fieldData.gameField[x][y]; + } + } + } + } + + /** + * Copy the contents of the buffer to this field data. + * @param buffer - the buffer to convert and copy + * @param startIndex - the start of the data to convert + */ + public void copyFromBuffer(byte[] buffer, int startIndex) { + byte[] shortBytes = new byte[2]; + + if (buffer != null) { + this.playerID = buffer[startIndex++]; + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.localActionID = toShort(shortBytes); + this.compressorSteps = buffer[startIndex++]; + this.launchBubbleColor = buffer[startIndex++]; + this.nextBubbleColor = buffer[startIndex++]; + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.attackBarBubbles = toShort(shortBytes); + + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 13; y++) { + this.gameField[x][y] = buffer[startIndex++]; + } + } + } + } + + /** + * Copy the contents of this field data to the buffer. + * @param buffer - the buffer to copy to + * @param startIndex - the start location to copy to + */ + public void copyToBuffer(byte[] buffer, int startIndex) { + byte[] shortBytes = new byte[2]; + + if (buffer != null) { + buffer[startIndex++] = this.playerID; + toByteArray(this.localActionID, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + buffer[startIndex++] = this.compressorSteps; + buffer[startIndex++] = this.launchBubbleColor; + buffer[startIndex++] = this.nextBubbleColor; + toByteArray(this.attackBarBubbles, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 13; y++) { + buffer[startIndex++] = this.gameField[x][y]; + } + } + } + } + }; + + /** + * This class encapsulates variables used to identify all possible + * player actions. + * @author Eric Fortin + * + */ + public class PlayerAction { + public byte playerID; // player ID associated with this action + public short localActionID; // ID of this particular action + public short remoteActionID; // ID of expected remote player action + /* + * The following three booleans are flags associated with player + * actions. + * + * compress - + * This flag indicates whether to lower the game field compressor. + * + * launchBubble - + * This flag indicates that the player desires a bubble launch to + * occur. This flag must be set with a valid aimPosition value, + * as well as valid values for launchBubbleColor and + * nextBubbleColor. + * + * swapBubble - + * This flag indicates that the player desires that the current + * launch bubble be swapped with the next launch bubble. This + * flag must be set with a valid aimPosition value, as well as + * valid values for launchBubbleColor and nextBubbleColor. + */ + public boolean compress; + public boolean launchBubble; + public boolean swapBubble; + public byte keyCode; + public byte launchBubbleColor; + public byte nextBubbleColor; + public byte newNextBubbleColor; + public short attackBarBubbles; + public byte attackBubbles[] = { -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1 }; + public double aimPosition; + + /** + * Class constructor. + * @param action - PlayerAction object to copy to this instance. + */ + public PlayerAction(PlayerAction action) { + copyFromAction(action); + } + + /** + * Class constructor. + * @param buffer - buffer contents to copy to this instance. + */ + public PlayerAction(byte[] buffer, int startIndex) { + copyFromBuffer(buffer, startIndex); + } + + /** + * Copy the contents of the supplied action to this action. + * @param action - the action to copy. + */ + public void copyFromAction(PlayerAction action) { + if (action != null) { + this.playerID = action.playerID; + this.localActionID = action.localActionID; + this.remoteActionID = action.remoteActionID; + this.compress = action.compress; + this.launchBubble = action.launchBubble; + this.swapBubble = action.swapBubble; + this.keyCode = action.keyCode; + this.launchBubbleColor = action.launchBubbleColor; + this.nextBubbleColor = action.nextBubbleColor; + this.newNextBubbleColor = action.newNextBubbleColor; + this.attackBarBubbles = action.attackBarBubbles; + + for (int index = 0; index < 15; index++) { + this.attackBubbles[index] = action.attackBubbles[index]; + } + + this.aimPosition = action.aimPosition; + } + } + + /** + * Copy the contents of the buffer to this action. + * @param buffer - the buffer to convert and copy. + * @param startIndex - the start of the data to convert. + */ + public void copyFromBuffer(byte[] buffer, int startIndex) { + byte[] shortBytes = new byte[2]; + byte[] doubleBytes = new byte[8]; + + if (buffer != null) { + this.playerID = buffer[startIndex++]; + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.localActionID = toShort(shortBytes); + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.remoteActionID = toShort(shortBytes); + this.compress = buffer[startIndex++] == 1; + this.launchBubble = buffer[startIndex++] == 1; + this.swapBubble = buffer[startIndex++] == 1; + this.keyCode = buffer[startIndex++]; + this.launchBubbleColor = buffer[startIndex++]; + this.nextBubbleColor = buffer[startIndex++]; + this.newNextBubbleColor = buffer[startIndex++]; + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.attackBarBubbles = toShort(shortBytes); + + for (int index = 0; index < 15; index++) { + this.attackBubbles[index] = buffer[startIndex++]; + } + + for (int index = 0; index < 8; index++) { + doubleBytes[index] = buffer[startIndex++]; + } + + this.aimPosition = toDouble(doubleBytes); + } + } + + /** + * Copy the contents of this action to the buffer. + * @param buffer - the buffer to copy to. + * @param startIndex - the start location to copy to. + */ + public void copyToBuffer(byte[] buffer, int startIndex) { + byte[] shortBytes = new byte[2]; + byte[] doubleBytes = new byte[8]; + + if (buffer != null) { + buffer[startIndex++] = this.playerID; + toByteArray(this.localActionID, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + toByteArray(this.remoteActionID, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + buffer[startIndex++] = (byte) ((this.compress == true)?1:0); + buffer[startIndex++] = (byte) ((this.launchBubble == true)?1:0); + buffer[startIndex++] = (byte) ((this.swapBubble == true)?1:0); + buffer[startIndex++] = this.keyCode; + buffer[startIndex++] = this.launchBubbleColor; + buffer[startIndex++] = this.nextBubbleColor; + buffer[startIndex++] = this.newNextBubbleColor; + toByteArray(this.attackBarBubbles, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + + for (int index = 0; index < 15; index++) { + buffer[startIndex++] = this.attackBubbles[index]; + } + + toByteArray(this.aimPosition, doubleBytes); + + for (int index = 0; index < 8; index++) { + buffer[startIndex++] = doubleBytes[index]; + } + } + } + }; + + /** + * This class encapsulates variables used to indicate the local game + * and player status, and is used to synchronize the exchange of + * information over the network. + *

This data is intended to be send periodically to the remote + * player(s) to keep all the players synchronized and informed of + * potential network issues with lost datagrams. This is especially + * common with multicasting, which is implemented via the User + * Datagram Protocol (UDP), which is unreliable.
+ * Refer to: + * http://en.wikipedia.org/wiki/User_Datagram_Protocol + * @author Eric Fortin + * + */ + public class PlayerStatus { + /* + * The following ID is the player associated with this status. + */ + public byte playerID; + public byte protocolVersion; + /* + * The following action IDs represent the associated player's + * current game state. localActionID will refer to that player's + * last transmitted action identifer, and remoteActionID will refer + * to that player's pending action identifier (the action it is + * expecting to receive next). + * + * This is useful for noting if a player has missed player action + * datagrams from another player, because its remoteActionID will be + * less than or equal to the localActionID of the other player if it + * has not received all the action transmissions from the other + * player(s). + */ + public short localActionID; + public short remoteActionID; + /* + * The following flag is used to manage game synchronization. + */ + public boolean readyToPlay; + /* + * The following flags are used to request data from the remote + * player(s) - either their game preferences, or game field data. + * When one or either of these flags is true, then the other + * player(s) shall transmit the appropriate information. + */ + private boolean fieldRequest; + private boolean prefsRequest; + /* + * The following values are the bubble grid CRC16 values for the + * local and remote game fields. When the CRC16 value is zero, the + * CRC16 value has not been calculated (or improbably, is zero). + */ + public short localChecksum; + public short remoteChecksum; + + /** + * Class constructor. + * @param id - the player ID associated with this status + * @param localId - the local last transmitted action ID. + * @param remoteId - the remote current pending action ID. + * @param ready - player is ready to play flag. + * @param field - request field data. + * @param prefs - request preference data. + * @param localCRC - the local player bubble grid CRC16 checksum. + * @param remoteCRC - the remote player bubble grid CRC16 checksum. + */ + public PlayerStatus(byte id, + short localId, + short remoteId, + boolean ready, + boolean field, + boolean prefs, + short localCRC, + short remoteCRC) { + init(id, localId, remoteId, ready, field, prefs, localCRC, remoteCRC); + } + + /** + * Class constructor. + * @param action - PlayerAction object to copy to this instance. + */ + public PlayerStatus(PlayerStatus status) { + copyFromStatus(status); + } + + /** + * Class constructor. + * @param buffer - buffer contents to copy to this instance. + */ + public PlayerStatus(byte[] buffer, int startIndex) { + copyFromBuffer(buffer, startIndex); + } + + /** + * Copy the contents of the supplied action to this action. + * @param action - the action to copy. + */ + public void copyFromStatus(PlayerStatus status) { + if (status != null) { + this.playerID = status.playerID; + this.protocolVersion = PROTOCOL_VERSION; + this.localActionID = status.localActionID; + this.remoteActionID = status.remoteActionID; + this.readyToPlay = status.readyToPlay; + this.fieldRequest = status.fieldRequest; + this.prefsRequest = status.prefsRequest; + this.localChecksum = status.localChecksum; + this.remoteChecksum = status.remoteChecksum; + } + } + + /** + * Copy the contents of the buffer to this status. + * @param buffer - the buffer to convert and copy. + * @param startIndex - the start of the data to convert. + */ + public void copyFromBuffer(byte[] buffer, int startIndex) { + byte[] shortBytes = new byte[2]; + + if (buffer != null) { + this.playerID = buffer[startIndex++]; + this.protocolVersion = buffer[startIndex++]; + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.localActionID = toShort(shortBytes); + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.remoteActionID = toShort(shortBytes); + this.readyToPlay = buffer[startIndex++] == 1; + this.fieldRequest = buffer[startIndex++] == 1; + this.prefsRequest = buffer[startIndex++] == 1; + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.localChecksum = toShort(shortBytes); + shortBytes[0] = buffer[startIndex++]; + shortBytes[1] = buffer[startIndex++]; + this.remoteChecksum = toShort(shortBytes); + } + } + + /** + * Copy the contents of this status to the buffer. + * @param buffer - the buffer to copy to. + * @param startIndex - the start location to copy to. + */ + public void copyToBuffer(byte[] buffer, int startIndex) { + byte[] shortBytes = new byte[2]; + + if (buffer != null) { + buffer[startIndex++] = this.playerID; + buffer[startIndex++] = this.protocolVersion; + toByteArray(this.localActionID, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + toByteArray(this.remoteActionID, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + buffer[startIndex++] = (byte) ((this.readyToPlay == true)?1:0); + buffer[startIndex++] = (byte) ((this.fieldRequest == true)?1:0); + buffer[startIndex++] = (byte) ((this.prefsRequest == true)?1:0); + toByteArray(this.localChecksum, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + toByteArray(this.remoteChecksum, shortBytes); + buffer[startIndex++] = shortBytes[0]; + buffer[startIndex++] = shortBytes[1]; + } + } + + /** + * Initialize this object with the provided data. + * @param id - the player ID associated with this status + * @param localId - the local last transmitted action ID. + * @param remoteId - the remote current pending action ID. + * @param ready - player is ready to play. + * @param field - request field data + * @param prefs - request preference data + * @param localCRC - the local player bubble grid CRC16 checksum. + * @param remoteCRC - the remote player bubble grid CRC16 checksum. + */ + public void init(byte id, + short localId, + short remoteId, + boolean ready, + boolean field, + boolean prefs, + short localCRC, + short remoteCRC) { + this.playerID = id; + this.protocolVersion = PROTOCOL_VERSION; + this.localActionID = localId; + this.remoteActionID = remoteId; + this.readyToPlay = ready; + this.fieldRequest = field; + this.prefsRequest = prefs; + this.localChecksum = localCRC; + this.remoteChecksum = remoteCRC; + } + }; + + private boolean actionTimerExpired() { + return System.currentTimeMillis() >= actionTxTime; + } + + /** + * Add a player action to the appropriate action list. Do not allow + * duplicate actions to populate the lists. + * @param newAction - the action to add to the appropriate list. + */ + private void addAction(PlayerAction newAction) { + if ((localPlayer != null) && (remotePlayer != null)) { + /* + * If an action is a local player action, add it to the action + * list if it is not already in the list. + * + * If it is a remote player action, add it to the action list if + * it is not already in the list. + */ + if (newAction.playerID == localPlayer.playerID) { + synchronized(localActionList) { + int listSize = localActionList.size(); + + for (int index = 0; index < listSize; index++) { + /* + * If a match is found, return from this function without + * adding the action to the list since it is a duplicate. + */ + if (localActionList.get(index).localActionID == + newAction.localActionID) { + return; + } + } + localActionList.add(newAction); + } + } + else if (newAction.playerID == remotePlayer.playerID) { + synchronized(remoteActionList) { + int listSize = remoteActionList.size(); + /* + * Update the remote player remote action ID to the ID of this + * action if it is exactly 1 greater than the local player + * local action ID. This signifies that the remote player has + * received all the local player action messages, since they + * are expecting an action datagram that has not yet been sent + * by the local player because it hasn't occurred. + */ + if (newAction.remoteActionID == + localStatus.localActionID + 1) { + remoteStatus.remoteActionID = newAction.remoteActionID; + } + + for (int index = 0; index < listSize; index++) { + /* + * If a match is found, return from this function without + * adding the action to the list since it is a duplicate. + */ + if (remoteActionList.get(index).localActionID == + newAction.localActionID) { + return; + } + } + /* + * Clear the list when the first action is received to remove + * spurious entries. + */ + if (newAction.localActionID == 1) { + remoteActionList.clear(); + } + remoteActionList.add(newAction); + } + /* + * If this action is the most current, then we can postpone the + * cyclic status message. This is because we just received the + * data that the status message is supposed to prompt the remote + * player to send. + */ + if (newAction.localActionID == localStatus.remoteActionID) { + setStatusTimeout(STATUS_TIMEOUT); + } + } + } + } + + public void checkRemoteChecksum() { + if ((localStatus != null) && (remoteStatus != null)) { + if ((localStatus.remoteActionID == (remoteStatus.localActionID + 1)) && + (localStatus.remoteChecksum != 0) && + (remoteStatus.localChecksum != 0) && + (localStatus.remoteChecksum != remoteStatus.localChecksum)) { + localStatus.fieldRequest = true; + } + } + } + + /** + * Check the local action list for actions that have been received + * by the remote player. When this is the case, the local action + * list entries will have action IDs lower than the remote player + * remote action ID. + *

Each call of this function will remove one entry. + * @return true if an entry was removed. + */ + private boolean cleanLocalActionList() { + boolean removed = false; + synchronized(localActionList) { + int listSize = localActionList.size(); + + for (int index = 0; index < listSize; index++) { + /* + * If the local action ID in the list is less than the remote + * player remote action ID, remove it from the list. Only one + * entry is removed per function call. + */ + if (localActionList.get(index).localActionID < + remoteStatus.remoteActionID) { + localActionList.remove(index); + removed = true; + break; + } + } + } + return removed; + } + + public void cleanUp() { + stopThread(); + + if (session != null) + session.cleanUp(); + session = null; + + /* + * Restore the local game preferences in the event that they were + * overwritten by the remote player's preferences. + */ + if (localPrefs != null) { + SharedPreferences sp = + myContext.getSharedPreferences(FrozenBubble.PREFS_NAME, + Context.MODE_PRIVATE); + PreferencesActivity.setFrozenBubblePrefs(localPrefs, sp); + } + + localPrefs = null; + remotePrefs = null; + localPlayer = null; + remotePlayer = null; + remoteGameFieldData = null; + remotePlayerAction = null; + + if (remoteInterface != null) + remoteInterface.cleanUp(); + remoteInterface = null; + + if (localActionList != null) + localActionList.clear(); + localActionList = null; + + if (remoteActionList != null) + remoteActionList.clear(); + remoteActionList = null; + } + + /** + * Copy the contents of the buffer to the designated preferences. + * @param buffer - the buffer to convert and copy. + * @param startIndex - the start of the data to convert. + */ + private void copyPrefsFromBuffer(Preferences prefs, + byte[] buffer, + int startIndex) { + byte[] intBytes = new byte[4]; + + if (buffer != null) { + intBytes[0] = buffer[startIndex++]; + intBytes[1] = buffer[startIndex++]; + intBytes[2] = buffer[startIndex++]; + intBytes[3] = buffer[startIndex++]; + prefs.collision = toInt(intBytes); + prefs.compressor = buffer[startIndex++] == 1; + intBytes[0] = buffer[startIndex++]; + intBytes[1] = buffer[startIndex++]; + intBytes[2] = buffer[startIndex++]; + intBytes[3] = buffer[startIndex++]; + prefs.difficulty = toInt(intBytes); + prefs.dontRushMe = buffer[startIndex++] == 1; + prefs.fullscreen = buffer[startIndex++] == 1; + intBytes[0] = buffer[startIndex++]; + intBytes[1] = buffer[startIndex++]; + intBytes[2] = buffer[startIndex++]; + intBytes[3] = buffer[startIndex++]; + prefs.gameMode = toInt(intBytes); + prefs.musicOn = buffer[startIndex++] == 1; + prefs.soundOn = buffer[startIndex++] == 1; + intBytes[0] = buffer[startIndex++]; + intBytes[1] = buffer[startIndex++]; + intBytes[2] = buffer[startIndex++]; + intBytes[3] = buffer[startIndex++]; + prefs.targetMode = toInt(intBytes); + } + } + + /** + * Copy the contents of this preferences object to the buffer. + * @param buffer - the buffer to copy to. + * @param startIndex - the start location to copy to. + */ + private void copyPrefsToBuffer(Preferences prefs, + byte[] buffer, + int startIndex) { + byte[] intBytes = new byte[4]; + + if (buffer != null) { + toByteArray(prefs.collision, intBytes); + buffer[startIndex++] = intBytes[0]; + buffer[startIndex++] = intBytes[1]; + buffer[startIndex++] = intBytes[2]; + buffer[startIndex++] = intBytes[3]; + buffer[startIndex++] = (byte) ((prefs.compressor == true)?1:0); + toByteArray(prefs.difficulty, intBytes); + buffer[startIndex++] = intBytes[0]; + buffer[startIndex++] = intBytes[1]; + buffer[startIndex++] = intBytes[2]; + buffer[startIndex++] = intBytes[3]; + buffer[startIndex++] = (byte) ((prefs.dontRushMe == true)?1:0); + buffer[startIndex++] = (byte) ((prefs.fullscreen == true)?1:0); + toByteArray(prefs.gameMode, intBytes); + buffer[startIndex++] = intBytes[0]; + buffer[startIndex++] = intBytes[1]; + buffer[startIndex++] = intBytes[2]; + buffer[startIndex++] = intBytes[3]; + buffer[startIndex++] = (byte) ((prefs.musicOn == true)?1:0); + buffer[startIndex++] = (byte) ((prefs.soundOn == true)?1:0); + toByteArray(prefs.targetMode, intBytes); + buffer[startIndex++] = intBytes[0]; + buffer[startIndex++] = intBytes[1]; + buffer[startIndex++] = intBytes[2]; + buffer[startIndex++] = intBytes[3]; + } + } + + /** + * Check if the network game is ready for action. The game is ready + * to begin play when all data synchronization tasks are completed, at + * which point every respective player's readyToPlay flag will be set. + * @return true if game synchronization is complete. + */ + public boolean gameIsReadyForAction() { + if (remoteStatus == null) + return false; + else + return localStatus.readyToPlay && remoteStatus.readyToPlay; + } + + private boolean gameStartTimerExpired() { + return System.currentTimeMillis() >= gameStartTime; + } + + private void getGameFieldData(GameFieldData gameData) { + FrozenGame gameRef = localPlayer.mGameRef; + + gameData.playerID = (byte) localPlayer.playerID; + gameData.localActionID = localStatus.localActionID; + gameData.compressorSteps = (byte) gameRef.getCompressorSteps(); + gameData.launchBubbleColor = (byte) gameRef.getCurrentColor(); + gameData.nextBubbleColor = (byte) gameRef.getNextColor(); + gameData.attackBarBubbles = (short) gameRef.getAttackBarBubbles(); + + BubbleSprite[][] bubbleGrid = gameRef.getGrid(); + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 13; j++) { + if (bubbleGrid[i][j] != null) { + gameData.gameField[i][j] = (byte) bubbleGrid[i][j].getColor(); + } + else { + gameData.gameField[i][j] = -1; + } + } + } + } + + public short getLatestRemoteActionId() { + if (remoteStatus != null) { + return remoteStatus.localActionID; + } + else { + return -1; + } + } + + /** + * Peek into the remote action list to see if we have obtained the + * current expected remote action. + * @return The reference to the current remote action if it exists, + * and null if we haven't received it yet. + */ + public PlayerAction getRemoteActionPreview() { + PlayerAction tempAction = null; + + synchronized(remoteActionList) { + int listSize = remoteActionList.size(); + + for (int index = 0; index < listSize; index++) { + /* + * When a match is found, return a reference to it. + */ + if (remoteActionList.get(index).localActionID == + localStatus.remoteActionID) { + tempAction = new PlayerAction(remoteActionList.get(index)); + break; + } + } + } + + return tempAction; + } + + /** + * This function obtains the expected remote player action (based on + * action ID) and places it into the remote player interface. + *

This function must be called periodically as it is assumed + * that the actions will be performed at the most appropriate time as + * determined by caller. + * @return true if the appropriate remote player action + * was retrieved from the remote action list. + */ + public boolean getRemoteAction() { + remoteInterface.gotAction = false; + synchronized(remoteActionList) { + int listSize = remoteActionList.size(); + + for (int index = 0; index < listSize; index++) { + /* + * When a match is found, copy the necessary element from the + * list, remove it, and exit the loop. + */ + if (remoteActionList.get(index).localActionID == + localStatus.remoteActionID) { + remoteInterface.playerAction.copyFromAction(remoteActionList.get(index)); + try { + remoteActionList.remove(index); + } catch (IndexOutOfBoundsException ioobe) { + ioobe.printStackTrace(); + } + remoteInterface.gotAction = true; + localStatus.remoteActionID++; + break; + } + } + } + + return remoteInterface.gotAction; + } + + private String getRemoteAddress() { + if (connectType == connectEnum.UDP_UNICAST) { + SharedPreferences dsp = + PreferenceManager.getDefaultSharedPreferences(myContext); + remoteIpAddress = dsp.getString("opponent_ip_address", null); + } + else + remoteIpAddress = MCAST_HOST_NAME; + + return remoteIpAddress; + } + + /** + * This function obtains the remote player interface and returns a + * reference to it to the caller. + * @return A reference to the remote player network game interface + * which provides all necessary remote player data. + */ + public RemoteInterface getRemoteInterface() { + return remoteInterface; + } + + /** + * This function is called from manager thread's run() + * method. This performs the network handshaking amongst peers to + * ensure proper game synchronization and operation. + */ + private void manageNetworkGame() { + if (localStatus == null) { + return; + } + /* + * If the game ID has not been reserved, check the current games in + * progress for an available game ID to reserve. + */ + if (myGameID == MulticastManager.FILTER_OFF) { + if (gameStartTimerExpired()) { + /* + * Don't reserve a game ID if we've never received a status + * message from another player. Either we are having network + * issues and may inadvertently reserve an already reserved ID, + * or there isn't even any one else to play with on the network. + */ + if (anyStatusRx) { + reserveGameID(); + } + else { + setGameStartTimeout(GAME_START_TIMEOUT); + } + } + } + + if (remoteStatus != null) { + /* + * On a new game, wait for the remote player to start a new game + * before requesting field data from the remote player. + */ + if (!gotFieldData && + !localStatus.fieldRequest && + !remoteStatus.readyToPlay) { + localStatus.fieldRequest = true; + } + /* + * If the last action transmitted by the local player has not yet + * been received by the remote player, the remote player remote + * action ID will match or be less than the local player local + * action ID. If this is the case, transmit the action ID + * expected by the remote player. + */ + if (localStatus.localActionID >= remoteStatus.remoteActionID) { + if (!missedAction) { + missedAction = true; + setActionTimeout(ACTION_TIMEOUT); + } + else if (actionTimerExpired()) { + sendLocalPlayerAction(remoteStatus.remoteActionID); + setActionTimeout(ACTION_TIMEOUT); + } + } + else { + missedAction = false; + } + + cleanLocalActionList(); + } + + /* + * Check whether various datagrams require transmission, such as + * player status, game field data, or preferences. + */ + if (statusTimerExpired()) { + if (remoteStatus != null) { + if (remoteStatus.prefsRequest) { + transmitPrefs(); + /* + * Clear the remote request flag to potentially reduce network + * overhead. If the remote player does not receive the data, + * the next remote status message will set the flag again. + */ + remoteStatus.prefsRequest = false; + } + + /* + * Only transmit the local game field if the local player local + * action ID is one less than the remote player remote action + * ID. This signifies that the distributed game is synchronized + * with respect to the local player. Actions must be executed + * synchronously with respect to the corresponding game field in + * order for performance to be identical on each distributed + * device. + */ + if (remoteStatus.fieldRequest && + ((localStatus.localActionID + 1) == remoteStatus.remoteActionID)) { + GameFieldData tempField = new GameFieldData(null); + getGameFieldData(tempField); + transmitGameField(tempField); + /* + * Clear the remote request flag to potentially reduce network + * overhead. If the remote player does not receive the data, + * the next remote status message will set the flag again. + */ + remoteStatus.fieldRequest = false; + } + } + + transmitStatus(localStatus); + setStatusTimeout(STATUS_TIMEOUT); + } + } + + public void newGame() { + gotFieldData = false; + if (localStatus != null) { + localStatus.readyToPlay = false; + localStatus.localActionID = 0; + localStatus.remoteActionID = 1; + localStatus.localChecksum = 0; + localStatus.remoteChecksum = 0; + } + if (localActionList != null) { + synchronized(localActionList) { + localActionList.clear(); + } + } + if (remoteActionList != null) { + synchronized(remoteActionList) { + remoteActionList.clear(); + } + } + /* + * Initialize the various timers. + */ + setActionTimeout(0L); + setGameStartTimeout(GAME_START_TIMEOUT); + setStatusTimeout(0L); + /* + * If a UDP session has not yet been created, create a new one and + * start the NetworkGameManager thread. + */ + if (session == null) { + try { + if (connectType == connectEnum.UDP_UNICAST) { + session = new MulticastManager(myContext, getRemoteAddress(), PORT); + } + else { + session = new MulticastManager(myContext, MCAST_BYTE_ADDR, PORT); + } + session.setMulticastListener(this); + } catch(UnknownHostException uhe) { + if (session != null) { + session.cleanUp(); + } + session = null; + } + /* + * Start the network manager thread. + */ + start(); + } + else { + /* + * Wake up the thread. + */ + synchronized(this) { + notify(); + } + } + } + + @Override + public void onMulticastEvent(eventEnum event, byte[] buffer, int length) { + /* + * Process the multicast message if it is a successfully received + * datagram that possesses a payload. + */ + if ((event == eventEnum.PACKET_RX) && (buffer != null)) { + /* + * The first three bytes of every message must contain the same + * three fields - the game ID, the message ID, and the player ID. + * The game ID and message ID are prefixed prior to each datagram + * transmission, as they are used by the network layer and are of + * no significance to any other module. The player ID must be the + * first byte of every datagram class implemented by the Frozen + * Bubble network game protocol. + * + * The game ID is used to filter messages so that only players + * with the same game ID will process each others' messages - all + * other messages are discarded. The exception is when the game + * ID is -1, which means that the player is attempting to join a + * game, so all incoming messages are processed until an + * unreserved game ID is found. + * + * The message ID is used to identify the message type - either a + * player status datagram, game field datagram, player preferences + * datagram, or a player action datagram. + * + * The player ID is used to identify who originated the message. + * This must be unique amongst all the players using the same game + * ID. + */ + byte gameId = buffer[0]; + byte msgId = buffer[1]; + byte playerId = buffer[2]; + + /* + * If the message contains the remote player status, copy it to + * the remote player status object. The remote player status + * object will be null until the first remote status datagram is + * received. + * + * If the game ID has not yet been set, then player status + * messages are the only messages that will be processed. + */ + if ((msgId == MSG_ID_STATUS) && (length == (STATUS_BYTES + 2))) { + anyStatusRx = true; + /* + * Perform game ID checking; otherwise process the remote player + * player status. + */ + if (myGameID == MulticastManager.FILTER_OFF) { + PlayerStatus tempStatus = new PlayerStatus(buffer, 2); + /* + * If we receive a status from a game already in progress, + * mark it and bump the game start timer. + * + * If we receive a status with the filter mask off and we have + * the same player ID as the player ID in that status, mark + * that game as already in progress and bump game start timer. + */ + if (tempStatus.readyToPlay || + ((gameId != MulticastManager.FILTER_OFF) && + (playerId == localPlayer.playerID))) { + if ((gameId >= 0) && (gameId < GAME_ID_MAX)) { + if (gamesInProgress[gameId] == false) { + gamesInProgress[gameId] = true; + setGameStartTimeout(GAME_START_TIMEOUT); + } + } + } + } + else if ((gameId != MulticastManager.FILTER_OFF) && + (playerId == remotePlayer.playerID)) { + if (remoteStatus == null) { + remoteStatus = new PlayerStatus(buffer, 2); + } + else { + remoteStatus.copyFromBuffer(buffer, 2); + } + } + } + + if (myGameID != MulticastManager.FILTER_OFF) { + /* + * If the message contains game preferences from player 1, then + * update the game preferences. The game preferences for all + * players are set per player 1. + */ + if ((msgId == MSG_ID_PREFS) && (length == (PREFS_BYTES + 3))) { + if ((playerId == VirtualInput.PLAYER1) && localStatus.prefsRequest) { + copyPrefsFromBuffer(remotePrefs, buffer, 3); + PreferencesActivity.setFrozenBubblePrefs(remotePrefs); + /* + * If all new game data synchronization requests have been + * fulfilled, then the network game is ready to begin. + */ + gotPrefsData = true; + localStatus.prefsRequest = false; + if (gotFieldData && + !localStatus.fieldRequest && + !localStatus.readyToPlay) { + localStatus.readyToPlay = true; + } + /* + * The local player status was updated. Set the status + * timeout to expire immediately and wake up the network + * manager thread. + */ + setStatusTimeout(0L); + synchronized(this) { + notify(); + } + } + } + + /* + * If the message contains a remote player game action, add it + * to the appropriate action list. + */ + if ((msgId == MSG_ID_ACTION) && (length == (ACTION_BYTES + 2))) { + if (playerId == remotePlayer.playerID) { + addAction(new PlayerAction(buffer, 2)); + } + } + + /* + * If the message contains the remote player game field, update + * the remote player interface game field object. + */ + if ((msgId == MSG_ID_FIELD) && (length == (FIELD_BYTES + 2))) { + if ((playerId == remotePlayer.playerID) && + localStatus.fieldRequest) { + remoteInterface.gameFieldData.copyFromBuffer(buffer, 2); + remoteInterface.gotFieldData = true; + /* + * If all new game data synchronization requests have been + * fulfilled, then the network game is ready to begin. + */ + gotFieldData = true; + localStatus.fieldRequest = false; + if (!localStatus.prefsRequest && !localStatus.readyToPlay) { + localStatus.readyToPlay = true; + } + /* + * The local player status was updated. Set the status + * timeout to expire immediately and wake up the network + * manager thread. + */ + setStatusTimeout(0L); + synchronized(this) { + notify(); + } + } + } + } + } + } + + public void pause() { + if (running) { + if (session != null) { + session.pause(); + } + paused = true; + } + } + + /** + * Reserve the first available game ID, and update the transport layer + * receive message filter to ignore all messages that don't have this + * game ID. + */ + public void reserveGameID() { + for (byte index = 0;index < GAME_ID_MAX;index++) { + if (!gamesInProgress[index]) { + myGameID = index; + if (session != null) { + session.setFilter(myGameID); + } + setStatusTimeout(0L); + break; + } + } + } + + /** + * This is the network game manager thread's run() call. + */ + @Override + public void run() { + paused = false; + running = true; + + while (running) + { + if (paused) try { + synchronized(this) { + wait(); + } + } catch (InterruptedException ie) { + /* + * Interrupted. This is expected behavior. + */ + } + + if (!paused && running) try { + synchronized(this) { + wait(100); + } + } catch (InterruptedException ie) { + /* + * Timed out. This is expected behavior. + */ + } + + if (!paused && running) { + manageNetworkGame(); + } + } + } + + /** + * Send the specified local player action from the local action list. + * @param actionId - the ID of the action to transmit. + */ + private void sendLocalPlayerAction(short actionId) { + synchronized(localActionList) { + int listSize = localActionList.size(); + + for (int index = 0; index < listSize; index++) { + /* + * If a match is found, transmit the action. + */ + if (localActionList.get(index).localActionID == actionId) { + transmitAction(localActionList.get(index)); + } + } + } + } + + /** + * Transmit the local player action to the remote player. The action + * counter identifier is incremented automatically. + * @param playerId - the local player ID. + * @param compress - set true to lower the compressor. + * @param launch - set true to launch a bubble. + * @param swap - set true to swap the launch bubble with + * the next bubble. + * @param keyCode - set to the key code value of the player key press. + * @param launchColor - the launch bubble color. + * @param nextColor - the next bubble color. + * @param newNextColor - when a bubble is launched, this is the new + * next bubble color. The prior next color is promoted to the + * launch bubble color. + * @param attackBarBubbles - the number of attack bubbles stored on + * the attack bar. If there are attack bubbles being launched, this + * should be the value prior to launch. + * @param attackBubbles - the array of attack bubble colors. A value + * of -1 denotes no color, and thus no attack bubble at that column. + * @param aimPosition - the launcher aim position. + */ + public void sendLocalPlayerAction(int playerId, + boolean compress, + boolean launch, + boolean swap, + int keyCode, + int launchColor, + int nextColor, + int newNextColor, + int attackBarBubbles, + byte attackBubbles[], + double aimPosition) { + PlayerAction tempAction = new PlayerAction(null); + tempAction.playerID = (byte) playerId; + tempAction.localActionID = ++localStatus.localActionID; + tempAction.remoteActionID = localStatus.remoteActionID; + tempAction.compress = compress; + tempAction.launchBubble = launch; + tempAction.swapBubble = swap; + tempAction.keyCode = (byte) keyCode; + tempAction.launchBubbleColor = (byte) launchColor; + tempAction.nextBubbleColor = (byte) nextColor; + tempAction.newNextBubbleColor = (byte) newNextColor; + tempAction.attackBarBubbles = (short) attackBarBubbles; + if (attackBubbles != null) + for (int index = 0;index < 15; index++) + tempAction.attackBubbles[index] = attackBubbles[index]; + tempAction.aimPosition = aimPosition; + addAction(tempAction); + transmitAction(tempAction); + } + + /** + * Set the action message timeout. + * @param timeout - the timeout expiration interval. + */ + public void setActionTimeout(long timeout) { + actionTxTime = System.currentTimeMillis() + timeout; + } + + /** + * Set the game start timeout. + * @param timeout - the timeout expiration interval. + */ + public void setGameStartTimeout(long timeout) { + gameStartTime = System.currentTimeMillis() + timeout; + } + + /** + * Set the local player local game field checksum. The checksum is + * set to zero immediately after every local player action, and must + * be set as soon as the game field has become static and a new + * checksum has been calculated. + * @param checksum - the new game field checksum. + */ + public void setLocalChecksum(short checksum) { + localStatus.localChecksum = checksum; + } + + /** + * Set the local player remote game field checksum. The checksum is + * set to zero immediately after every remote player action, and must + * be set as soon as the game field has become static and a new + * checksum has been calculated. + * @param checksum - the new game field checksum. + */ + public void setRemoteChecksum(short checksum) { + localStatus.remoteChecksum = checksum; + } + + /** + * Set the status message timeout. + * @param timeout - the timeout expiration interval. + */ + public void setStatusTimeout(long timeout) { + statusTxTime = System.currentTimeMillis() + timeout; + } + + private boolean statusTimerExpired() { + return System.currentTimeMillis() >= statusTxTime; + } + + /** + * Stop and join() the network game manager thread. + */ + private void stopThread() { + paused = false; + running = false; + synchronized(this) { + interrupt(); + } + /* + * Close and join() the multicast thread. + */ + boolean retry = true; + while (retry) { + try { + join(); + retry = false; + } catch (InterruptedException e) { + /* + * Keep trying to close the multicast thread. + */ + } + } + } + + /** + * Populate a byte array with the byte representation of a short. + * The byte array must consist of at least 2 bytes. + * @param value - the short to convert to a byte array. + * @param array - the byte array where the converted short is placed. + */ + public static void toByteArray(short value, byte[] array) { + ByteBuffer.wrap(array).putShort(value); + } + + /** + * Populate a byte array with the byte representation of an integer. + * The byte array must consist of at least 4 bytes. + * @param value - the integer to convert to a byte array. + * @param array - the byte array where the converted int is placed. + */ + public static void toByteArray(int value, byte[] array) { + ByteBuffer.wrap(array).putInt(value); + } + + /** + * Populate a byte array with the byte representation of a double. + * The byte array must consist of at least 8 bytes. + * @param value - the double to convert to a byte array. + * @param array - the byte array where the converted double is placed. + */ + public static void toByteArray(double value, byte[] array) { + ByteBuffer.wrap(array).putDouble(value); + } + + /** + * Convert a byte array into a double value. + * @param bytes - the byte array to convert into a double. + * @return The double representation of the supplied byte array. + */ + public static double toDouble(byte[] bytes) { + return ByteBuffer.wrap(bytes).getDouble(); + } + + /** + * Convert a byte array into an integer value. + * @param bytes - the byte array to convert into an integer. + * @return The double representation of the supplied byte array. + */ + public static int toInt(byte[] bytes) { + return ByteBuffer.wrap(bytes).getInt(); + } + + /** + * Convert a byte array into a short value. + * @param bytes - the byte array to convert into a short. + * @return The short representation of the supplied byte array. + */ + public static short toShort(byte[] bytes) { + return ByteBuffer.wrap(bytes).getShort(); + } + + /** + * Transmit the local player action to the remote player via the + * network interface. + * @param action - the player action to transmit. + * @return true if the transmission was successful. + */ + private boolean transmitAction(PlayerAction action) { + byte[] buffer = new byte[ACTION_BYTES + 2]; + buffer[0] = myGameID; + buffer[1] = MSG_ID_ACTION; + action.copyToBuffer(buffer, 2); + /* + * Send the datagram via the multicast manager. + */ + if (session != null) { + return session.transmit(buffer); + } + else { + return false; + } + } + + /** + * Transmit the local player game field to the remote player via the + * network interface. + * @param gameField - the player game field data to transmit. + * @return true if the transmission was successful. + */ + private boolean transmitGameField(GameFieldData gameField) { + byte[] buffer = new byte[FIELD_BYTES + 2]; + buffer[0] = myGameID; + buffer[1] = MSG_ID_FIELD; + gameField.copyToBuffer(buffer, 2); + /* + * Send the datagram via the multicast manager. + */ + if (session != null) { + return session.transmit(buffer); + } + else { + return false; + } + } + + /** + * Transmit the player status message. + * @return true if the transmission was successful. + */ + private boolean transmitStatus(PlayerStatus status) { + byte[] buffer = new byte[STATUS_BYTES + 2]; + buffer[0] = myGameID; + buffer[1] = MSG_ID_STATUS; + status.copyToBuffer(buffer, 2); + /* + * Send the datagram via the multicast manager. + */ + if (session != null) { + return session.transmit(buffer); + } + else { + return false; + } + } + + /** + * Transmit the local player preferences to the remote player via the + * network interface. + * @return true if the transmission was successful. + */ + private boolean transmitPrefs() { + byte[] buffer = new byte[Preferences.PREFS_BYTES + 3]; + buffer[0] = myGameID; + buffer[1] = MSG_ID_PREFS; + buffer[2] = (byte) localPlayer.playerID; + copyPrefsToBuffer(localPrefs, buffer, 3); + /* + * Send the datagram via the multicast manager. + */ + if (session != null) { + return session.transmit(buffer); + } + else { + return false; + } + } + + public void unPause() { + paused = false; + synchronized(this) { + interrupt(); + } + if (session != null) { + session.unPause(); + } + } + + public void updateNetworkStatus(NetworkStatus status) { + status.localPlayerId = localPlayer.playerID; + status.remotePlayerId = remotePlayer.playerID; + if (session != null) { + status.isConnected = session.hasInternetConnection(); + } + else { + status.isConnected = false; + } + status.reservedGameId = myGameID != MulticastManager.FILTER_OFF; + status.playerJoined = remoteStatus != null; + if (localStatus != null) { + status.gotFieldData = gotFieldData; + status.gotPrefsData = gotPrefsData; + } + else { + status.gotFieldData = false; + status.gotPrefsData = false; + } + status.readyToPlay = gameIsReadyForAction(); + if ((localIpAddress == null) && (session != null)) { + localIpAddress = session.getLocalIpAddress(); + } + status.localIpAddress = localIpAddress; + if (remoteIpAddress == null) { + getRemoteAddress(); + } + status.remoteIpAddress = remoteIpAddress; + } +}; diff --git a/src/com/efortin/frozenbubble/Preferences.java b/src/com/efortin/frozenbubble/Preferences.java new file mode 100644 index 0000000..84f1935 --- /dev/null +++ b/src/com/efortin/frozenbubble/Preferences.java @@ -0,0 +1,120 @@ +/* + * [[ Frozen-Bubble ]] + * + * Copyright (c) 2000-2003 Guillaume Cottenceau. + * Java sourcecode - Copyright (c) 2003 Glenn Sanson. + * Additional source - Copyright (c) 2013 Eric Fortin. + * + * This code is distributed under the GNU General Public License + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 or 3, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to: + * Free Software Foundation, Inc. + * 675 Mass Ave + * Cambridge, MA 02139, USA + * + * Artwork: + * Alexis Younes <73lab at free.fr> + * (everything but the bubbles) + * Amaury Amblard-Ladurantie + * (the bubbles) + * + * Soundtrack: + * Matthias Le Bidan + * (the three musics and all the sound effects) + * + * Design & Programming: + * Guillaume Cottenceau + * (design and manage the project, whole Perl sourcecode) + * + * Java version: + * Glenn Sanson + * (whole Java sourcecode, including JIGA classes + * http://glenn.sanson.free.fr/jiga/) + * + * Android port: + * Pawel Aleksander Fedorynski + * Eric Fortin + * Copyright (c) Google Inc. + * + * [[ http://glenn.sanson.free.fr/fb/ ]] + * [[ http://www.frozen-bubble.org/ ]] + */ + +package com.efortin.frozenbubble; + +import org.jfedor.frozenbubble.BubbleSprite; +import org.jfedor.frozenbubble.FrozenBubble; +import org.jfedor.frozenbubble.LevelManager; + +public class Preferences { + public static final int PREFS_BYTES = 22; + + boolean adsOn = true; + int collision = BubbleSprite.MIN_PIX; + boolean colorMode = false; + boolean compressor = false; + int difficulty = LevelManager.MODERATE; + boolean dontRushMe = true; + boolean fullscreen = true; + int gameMode = FrozenBubble.GAME_NORMAL; + boolean musicOn = true; + boolean soundOn = true; + int targetMode = FrozenBubble.POINT_TO_SHOOT; + + /** + * Preferences class constructor. Variables are + * initialized to defaults. + */ + public Preferences() { + adsOn = true; + collision = BubbleSprite.MIN_PIX; + colorMode = false; + compressor = false; + difficulty = LevelManager.MODERATE; + dontRushMe = true; + fullscreen = true; + gameMode = FrozenBubble.GAME_NORMAL; + musicOn = true; + soundOn = true; + targetMode = FrozenBubble.POINT_TO_SHOOT; + } + + /** + * Preferences class constructor. + * @param prefs - object reference used to initialize this object. + * Pass null to create a default instance. + */ + public Preferences(Preferences prefs) { + copy(prefs); + } + + /** + * Copy the values of the supplied object to this object. + * @param prefs - the object to copy to this object. + */ + public void copy(Preferences prefs) { + if (prefs != null) { + this.adsOn = prefs.adsOn; + this.collision = prefs.collision; + this.colorMode = prefs.colorMode; + this.compressor = prefs.compressor; + this.difficulty = prefs.difficulty; + this.dontRushMe = prefs.dontRushMe; + this.fullscreen = prefs.fullscreen; + this.gameMode = prefs.gameMode; + this.musicOn = prefs.musicOn; + this.soundOn = prefs.soundOn; + this.targetMode = prefs.targetMode; + } + } +}; diff --git a/src/com/efortin/frozenbubble/PreferencesActivity.java b/src/com/efortin/frozenbubble/PreferencesActivity.java index c081a57..bb554e8 100644 --- a/src/com/efortin/frozenbubble/PreferencesActivity.java +++ b/src/com/efortin/frozenbubble/PreferencesActivity.java @@ -65,46 +65,47 @@ import android.view.KeyEvent; public class PreferencesActivity extends PreferenceActivity{ - private boolean adsOn = true; - private int collision = BubbleSprite.MIN_PIX; - private boolean compressor = false; - private int difficulty = LevelManager.MODERATE; - private boolean dontRushMe = false; - private boolean fullscreen = true; - private boolean colorMode = false; - private int gameMode = FrozenBubble.GAME_NORMAL; - private boolean musicOn = true; - private boolean soundOn = true; - private int targetMode = FrozenBubble.POINT_TO_SHOOT; - - private void getFrozenBubblePrefs() { - SharedPreferences mConfig = getSharedPreferences(FrozenBubble.PREFS_NAME, - Context.MODE_PRIVATE); - adsOn = mConfig.getBoolean("adsOn", true ); - collision = mConfig.getInt ("collision", BubbleSprite.MIN_PIX ); - compressor = mConfig.getBoolean("compressor", false ); - difficulty = mConfig.getInt ("difficulty", LevelManager.MODERATE ); - dontRushMe = mConfig.getBoolean("dontRushMe", false ); - fullscreen = mConfig.getBoolean("fullscreen", true ); - gameMode = mConfig.getInt ("gameMode", FrozenBubble.GAME_NORMAL ); - musicOn = mConfig.getBoolean("musicOn", true ); - soundOn = mConfig.getBoolean("soundOn", true ); - targetMode = mConfig.getInt ("targetMode", FrozenBubble.POINT_TO_SHOOT); - - if (gameMode == FrozenBubble.GAME_NORMAL) - colorMode = false; + + private Preferences mPrefs; + + private void cleanUp() { + mPrefs = null; + } + + public static void getFrozenBubblePrefs(Preferences prefs, SharedPreferences sp) { + prefs.adsOn = sp.getBoolean("adsOn", true ); + prefs.collision = sp.getInt ("collision", BubbleSprite.MIN_PIX ); + prefs.compressor = sp.getBoolean("compressor", false ); + prefs.difficulty = sp.getInt ("difficulty", LevelManager.MODERATE ); + prefs.dontRushMe = sp.getBoolean("dontRushMe", false ); + prefs.fullscreen = sp.getBoolean("fullscreen", true ); + prefs.gameMode = sp.getInt ("gameMode", FrozenBubble.GAME_NORMAL ); + prefs.musicOn = sp.getBoolean("musicOn", true ); + prefs.soundOn = sp.getBoolean("soundOn", true ); + prefs.targetMode = sp.getInt ("targetMode", FrozenBubble.POINT_TO_SHOOT); + + if (prefs.gameMode == FrozenBubble.GAME_NORMAL) + prefs.colorMode = false; else - colorMode = true; + prefs.colorMode = true; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mPrefs = new Preferences(); setDefaultPreferences(); addPreferencesFromResource(R.layout.activity_preferences_screen); } + @Override + protected void onDestroy() { + super.onDestroy(); + + cleanUp(); + } + @Override public boolean onKeyDown(int keyCode, KeyEvent msg) { if (keyCode == KeyEvent.KEYCODE_BACK) { @@ -115,73 +116,90 @@ public boolean onKeyDown(int keyCode, KeyEvent msg) { } private void savePreferences() { - SharedPreferences prefs = + SharedPreferences dsp = PreferenceManager.getDefaultSharedPreferences(this); - adsOn = prefs.getBoolean("ads_option", true); - collision = prefs.getInt("collision_option", BubbleSprite.MIN_PIX); - compressor = prefs.getBoolean("compressor_option", false); - difficulty = prefs.getInt("difficulty_option", LevelManager.MODERATE); - dontRushMe = !prefs.getBoolean("rush_me_option", true); - fullscreen = prefs.getBoolean("fullscreen_option", true); - colorMode = prefs.getBoolean("colorblind_option", false); - musicOn = prefs.getBoolean("play_music_option", true); - soundOn = prefs.getBoolean("sound_effects_option", true); - targetMode = Integer.valueOf(prefs.getString("targeting_option", + mPrefs.adsOn = dsp.getBoolean("ads_option", true); + mPrefs.collision = dsp.getInt("collision_option", BubbleSprite.MIN_PIX); + mPrefs.compressor = dsp.getBoolean("compressor_option", false); + mPrefs.difficulty = dsp.getInt("difficulty_option", LevelManager.MODERATE); + mPrefs.dontRushMe = !dsp.getBoolean("rush_me_option", true); + mPrefs.fullscreen = dsp.getBoolean("fullscreen_option", true); + mPrefs.colorMode = dsp.getBoolean("colorblind_option", false); + mPrefs.musicOn = dsp.getBoolean("play_music_option", true); + mPrefs.soundOn = dsp.getBoolean("sound_effects_option", true); + mPrefs.targetMode = Integer.valueOf(dsp.getString("targeting_option", Integer.toString(FrozenBubble.POINT_TO_SHOOT))); - if (!colorMode) - gameMode = FrozenBubble.GAME_NORMAL; + if (!mPrefs.colorMode) + mPrefs.gameMode = FrozenBubble.GAME_NORMAL; else - gameMode = FrozenBubble.GAME_COLORBLIND; + mPrefs.gameMode = FrozenBubble.GAME_COLORBLIND; - setFrozenBubblePrefs(); + setFrozenBubblePrefs(mPrefs); + + SharedPreferences sp = getSharedPreferences(FrozenBubble.PREFS_NAME, + Context.MODE_PRIVATE); + + setFrozenBubblePrefs(mPrefs, sp); } private void setDefaultPreferences() { - getFrozenBubblePrefs(); + SharedPreferences sp = getSharedPreferences(FrozenBubble.PREFS_NAME, + Context.MODE_PRIVATE); + getFrozenBubblePrefs(mPrefs, sp); - SharedPreferences prefs = + SharedPreferences spEditor = PreferenceManager.getDefaultSharedPreferences(this); - SharedPreferences.Editor editor = prefs.edit(); - - editor.putBoolean("ads_option", adsOn); - editor.putInt("collision_option", collision); - editor.putBoolean("compressor_option", compressor); - editor.putInt("difficulty_option", difficulty); - editor.putBoolean("rush_me_option", !dontRushMe); - editor.putBoolean("fullscreen_option", fullscreen); - editor.putBoolean("colorblind_option", colorMode); - editor.putBoolean("play_music_option", musicOn); - editor.putBoolean("sound_effects_option", soundOn); - editor.putString("targeting_option", Integer.toString(targetMode)); + SharedPreferences.Editor editor = spEditor.edit(); + editor.putBoolean("ads_option", mPrefs.adsOn); + editor.putInt("collision_option", mPrefs.collision); + editor.putBoolean("compressor_option", mPrefs.compressor); + editor.putInt("difficulty_option", mPrefs.difficulty); + editor.putBoolean("rush_me_option", !mPrefs.dontRushMe); + editor.putBoolean("fullscreen_option", mPrefs.fullscreen); + editor.putBoolean("colorblind_option", mPrefs.colorMode); + editor.putBoolean("play_music_option", mPrefs.musicOn); + editor.putBoolean("sound_effects_option", mPrefs.soundOn); + editor.putString("targeting_option", Integer.toString(mPrefs.targetMode)); editor.commit(); } - private void setFrozenBubblePrefs() { - FrozenBubble.setAdsOn(adsOn); - FrozenBubble.setCollision(collision); - FrozenBubble.setCompressor(compressor); - FrozenBubble.setDifficulty(difficulty); - FrozenBubble.setDontRushMe(dontRushMe); - FrozenBubble.setFullscreen(fullscreen); - FrozenBubble.setMode(gameMode); - FrozenBubble.setMusicOn(musicOn); - FrozenBubble.setSoundOn(soundOn); - FrozenBubble.setTargetMode(targetMode); + /** + * Update the game preferences to the desired values. + * @param prefs - the desired game preferences. + */ + public static void setFrozenBubblePrefs(Preferences prefs) { + FrozenBubble.setAdsOn(prefs.adsOn); + FrozenBubble.setCollision(prefs.collision); + FrozenBubble.setCompressor(prefs.compressor); + FrozenBubble.setDifficulty(prefs.difficulty); + FrozenBubble.setDontRushMe(prefs.dontRushMe); + FrozenBubble.setFullscreen(prefs.fullscreen); + FrozenBubble.setMode(prefs.gameMode); + FrozenBubble.setMusicOn(prefs.musicOn); + FrozenBubble.setSoundOn(prefs.soundOn); + FrozenBubble.setTargetMode(prefs.targetMode); + } - SharedPreferences sp = getSharedPreferences(FrozenBubble.PREFS_NAME, - Context.MODE_PRIVATE); + /** + * Save the desired game preference values to nonvolatile memory. + * @param prefs - the desired game preferences. + * @param sp - the SharedPreferences object reference to + * create a preference editor for the purpose of saving the + * preferences to nonvolatile memory. + */ + public static void setFrozenBubblePrefs(Preferences prefs, SharedPreferences sp) { SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean("adsOn", adsOn); - editor.putInt("collision", collision); - editor.putBoolean("compressor", compressor); - editor.putInt("difficulty", difficulty); - editor.putBoolean("dontRushMe", dontRushMe); - editor.putBoolean("fullscreen", fullscreen); - editor.putInt("gameMode", gameMode); - editor.putBoolean("musicOn", musicOn); - editor.putBoolean("soundOn", soundOn); - editor.putInt("targetMode", targetMode); + editor.putBoolean("adsOn", prefs.adsOn); + editor.putInt("collision", prefs.collision); + editor.putBoolean("compressor", prefs.compressor); + editor.putInt("difficulty", prefs.difficulty); + editor.putBoolean("dontRushMe", prefs.dontRushMe); + editor.putBoolean("fullscreen", prefs.fullscreen); + editor.putInt("gameMode", prefs.gameMode); + editor.putBoolean("musicOn", prefs.musicOn); + editor.putBoolean("soundOn", prefs.soundOn); + editor.putInt("targetMode", prefs.targetMode); editor.commit(); } } diff --git a/src/com/efortin/frozenbubble/ScrollingCredits.java b/src/com/efortin/frozenbubble/ScrollingCredits.java index 14c2f49..c658dc5 100644 --- a/src/com/efortin/frozenbubble/ScrollingCredits.java +++ b/src/com/efortin/frozenbubble/ScrollingCredits.java @@ -158,15 +158,12 @@ private void displayImage(int id) { * Set the window layout according to the settings in the specified * layout XML file. Then apply the full screen option according to * the player preference setting. - * *

Note that the title bar is not desired for the scrolling * credits, and requesting that the title bar be removed must * be applied before setting the view content by applying the XML * layout or it will generate an exception. - * - * @param layoutResID - * - The resource ID of the XML layout to use for the window - * layout settings. + * @param layoutResID - The resource ID of the XML layout to use for + * the window layout settings. */ private void setWindowLayout(int layoutResID) { final int flagFs = WindowManager.LayoutParams.FLAG_FULLSCREEN; @@ -208,18 +205,16 @@ public void cleanUp() { public void end() { credits.abort(); - // - // Since the default game activity creates its own player, - // destroy the current player. - // - // + /* + * Since the default game activity creates its own player, + * destroy the current player. + */ cleanUp(); - // - // Create an intent to launch the game activity. Since it was - // running in the background while this activity was running, it - // may have been stopped by the system. - // - // + /* + * Create an intent to launch the game activity. Since it was + * running in the background while this activity was running, it + * may have been stopped by the system. + */ Intent intent = new Intent(this, FrozenBubble.class); startActivity(intent); finish(); @@ -239,7 +234,9 @@ public void resumeCredits() { @Override public void run() { - // Check if we need to display the end of game victory image. + /* + * Check if we need to display the end of game victory image. + */ if (!credits.isScrolling() && !victoryScreenShown) { victoryScreenShown = true; // Make the credits text transparent. diff --git a/src/com/efortin/frozenbubble/ScrollingTextView.java b/src/com/efortin/frozenbubble/ScrollingTextView.java index 0081c39..1c287a7 100644 --- a/src/com/efortin/frozenbubble/ScrollingTextView.java +++ b/src/com/efortin/frozenbubble/ScrollingTextView.java @@ -207,8 +207,8 @@ public float getSpeed() { } public boolean isScrolling() { - return ((scrollCount != 0) || !scroller.isFinished() || - scrollingPaused || !started); + return (scrollCount != 0) || !scroller.isFinished() || scrollingPaused || + !started; } public void setPaused(boolean paused) { diff --git a/src/com/efortin/frozenbubble/SeekBarPreference.java b/src/com/efortin/frozenbubble/SeekBarPreference.java index f0e4c4c..907cb4d 100644 --- a/src/com/efortin/frozenbubble/SeekBarPreference.java +++ b/src/com/efortin/frozenbubble/SeekBarPreference.java @@ -180,7 +180,7 @@ public void onBindView(View view) { } /** - * Update a SeekBarPreference view with our current state + * Update a SeekBarPreference view with our current state. * @param view */ protected void updateView(View view) { diff --git a/src/com/efortin/frozenbubble/VirtualInput.java b/src/com/efortin/frozenbubble/VirtualInput.java new file mode 100644 index 0000000..0f4b28e --- /dev/null +++ b/src/com/efortin/frozenbubble/VirtualInput.java @@ -0,0 +1,168 @@ +/* + * [[ Frozen-Bubble ]] + * + * Copyright (c) 2000-2003 Guillaume Cottenceau. + * Java sourcecode - Copyright (c) 2003 Glenn Sanson. + * Additional source - Copyright (c) 2013 Eric Fortin. + * + * This code is distributed under the GNU General Public License + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 or 3, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to: + * Free Software Foundation, Inc. + * 675 Mass Ave + * Cambridge, MA 02139, USA + * + * Artwork: + * Alexis Younes <73lab at free.fr> + * (everything but the bubbles) + * Amaury Amblard-Ladurantie + * (the bubbles) + * + * Soundtrack: + * Matthias Le Bidan + * (the three musics and all the sound effects) + * + * Design & Programming: + * Guillaume Cottenceau + * (design and manage the project, whole Perl sourcecode) + * + * Java version: + * Glenn Sanson + * (whole Java sourcecode, including JIGA classes + * http://glenn.sanson.free.fr/jiga/) + * + * Android port: + * Pawel Aleksander Fedorynski + * Eric Fortin + * Copyright (c) Google Inc. + * + * [[ http://glenn.sanson.free.fr/fb/ ]] + * [[ http://www.frozen-bubble.org/ ]] + */ + +package com.efortin.frozenbubble; + +import org.jfedor.frozenbubble.FrozenGame; + +import android.view.KeyEvent; + +/** + * This class encapsulates variables used to interface all possible + * virtual player actions. + * @author Eric Fortin + * + */ +public abstract class VirtualInput { + /* + * Player ID definitions. + */ + public static final byte PLAYER1 = 1; + public static final byte PLAYER2 = 2; + + public int playerID = PLAYER1; + public boolean isCPU = false; + public boolean isRemote = false; + public FrozenGame mGameRef = null; + protected boolean mTouchFire = false; + protected boolean mWasCenter = false; + protected boolean mWasDown = false; + protected boolean mWasLeft = false; + protected boolean mWasRight = false; + protected boolean mWasUp = false; + + /* + * The following are abstract methods that must be implemented by + * descendants. They must clear the appropriate action flag and + * return its original value. + */ + public abstract boolean actionCenter(); + public abstract boolean actionDown(); + public abstract boolean actionLeft(); + public abstract boolean actionRight(); + public abstract boolean actionUp(); + + /* The following are abstract methods that must be implemented by + * descendants to handle all the various input events. + */ + public abstract boolean checkNewActionKeyPress(int keyCode); + public abstract boolean setKeyDown(int keyCode); + public abstract boolean setKeyUp(int keyCode); + public abstract boolean setTouchEvent(int event, double x, double y); + public abstract void setTrackBallDx(double trackBallDX); + + /** + * Configure this player input instance. + * @param id - this player ID, e.g., PLAYER1. + * @param type - true if this player is a CPU simulation. + * @param remote - true if this player is playing on a + * remote machine, false if this player is local. + * @see VirtualInput + */ + protected final void configure(int id, + boolean type, + boolean remote) { + playerID = id; + isCPU = type; + isRemote = remote; + } + + /** + * Initialize class variables to defaults. + */ + public final void init_vars() { + mGameRef = null; + mTouchFire = false; + mWasCenter = false; + mWasDown = false; + mWasLeft = false; + mWasRight = false; + mWasUp = false; + } + + /** + * Process virtual key presses. This method only sets the + * historical keypress flags, which must be cleared by ancestors + * that inherit this class. + * @param keyCode + */ + public final void setAction(int keyCode, boolean touch) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + mWasCenter = true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mWasDown = true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mWasLeft = true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mWasRight = true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + if (touch) { + mTouchFire = true; + } + else { + mWasUp = true; + } + } + } + + /** + * Set the game reference for this player. + * @param gameRef - the reference to this player's game object. + */ + public final void setGameRef(FrozenGame gameRef) { + mGameRef = gameRef; + } +} diff --git a/src/com/peculiargames/andmodplug/MODResourcePlayer.java b/src/com/peculiargames/andmodplug/MODResourcePlayer.java index 50b70c1..56ba2ab 100644 --- a/src/com/peculiargames/andmodplug/MODResourcePlayer.java +++ b/src/com/peculiargames/andmodplug/MODResourcePlayer.java @@ -61,31 +61,27 @@ /** * Convenience class extending PlayerThread, handling all the file * operations, accepting Android resource IDs for MOD/XM song files. - * - *

Typical call order: - *
// get player instance (in topmost activity, etc.) - *
mrp = MODResourcePlayer(); - *
// load MOD/XM data into player - *
mrp.LoadMODResource(R.raw.coolsong); - *
mrp.start(); // start thread (playing song)
- *
- * Then when changing songs (new game level or transition to - * another sub-activity, etc.): - *
mrp.PausePlay(); - *
mrp.LoadMODResource(R.raw.newcoolsong); - *
mrp.UnPausePlay(); - *
// repeat...
- * - * @version 1.0 - * - * @author P.A. Casey (crow) Peculiar-Games.com + *

Typical call order: + *
// get player instance (in topmost activity, etc.) + *
mrp = MODResourcePlayer(); + *
// load MOD/XM data into player + *
mrp.LoadMODResource(R.raw.coolsong); + *
mrp.start(); // start thread (playing song)
+ *
Then when changing songs (new game level or transition to + * another sub-activity, etc.): + *
mrp.PausePlay(); + *
mrp.LoadMODResource(R.raw.newcoolsong); + *
mrp.UnPausePlay(); + *
// repeat...
+ * @version 1.0 + * @author P.A. Casey (crow) Peculiar-Games.com + * */ public class MODResourcePlayer extends PlayerThread { - // - // Application context, for accessing the resources - specifically the - // the R.raw. resources which are MOD music files. - // - // + /* + * Application context, for accessing the resources - specifically the + * the R.raw. resources which are MOD music files. + */ private Context mContext; /** @@ -93,17 +89,14 @@ public class MODResourcePlayer extends PlayerThread { * resource files (typically the songs are stored in the res/raw * project directory and conform to Android build process rules, * lower-case names, etc.) - * - *

Note about extensions: - *
Developers using Eclipse as an IDE should note that it allows - * the .xm file extension but may be fussy about other tracker - * format extensions. - * - *

The context argument is the application context - * which allows MODResourcePlayer to load resources directly. - * - * @param context - * - Application context that is creating this instance. + *

Note about extensions: + *
Developers using Eclipse as an IDE should note that it allows + * the .xm file extension but may be fussy about other tracker format + * extensions. + *

The context argument is the application context + * which allows MODResourcePlayer to load resources directly. + * @param context - Application context that is creating this + * instance. */ public MODResourcePlayer(Context context) { // Get super class (PlayerThread) with default rate. @@ -115,34 +108,28 @@ public MODResourcePlayer(Context context) { /** * Load a MOD/XM/etc. song file from an Android resource. - * - *

Note about extensions: - *
Developers using Eclipse as an IDE should note that it allows - * the .xm file extension but may be fussy about other tracker - * format extensions. - * - *

The modresource argument is the resource id for - * the MOD/XM song file, e.g. R.raw.coolsong - * - * @param modresource - * - Android resource id for a MOD/XM/etc. (tracker format) - * song file. + *

Note about extensions: + *
Developers using Eclipse as an IDE should note that it allows + * the .xm file extension but may be fussy about other tracker format + * extensions. + *

The modresource argument is the resource id for the + * MOD/XM song file, e.g. R.raw.coolsong + * @param modresource - Android resource id for a MOD/XM/etc. (tracker + * format) song file. */ public boolean LoadMODResource(int modresource) { byte[] modData = null; int currfilesize = 0; InputStream mModfileInStream; - // - // Unload any mod file we have currently loaded. - // - // + /* + * Unload any mod file we have currently loaded. + */ UnLoadMod(); - // - // Get an input stream for the MOD file resource. - // - // + /* + * Get an input stream for the MOD file resource. + */ mModfileInStream = mContext.getResources().openRawResource(modresource); try { currfilesize = mModfileInStream.available(); @@ -150,10 +137,9 @@ public boolean LoadMODResource(int modresource) { return false; } - // - // Allocate a buffer that can hold the current MOD file data. - // - // + /* + * Allocate a buffer that can hold the current MOD file data. + */ modData = new byte[currfilesize]; try { @@ -163,10 +149,9 @@ public boolean LoadMODResource(int modresource) { e.printStackTrace(); } - // - // Load the song into the player. - // - // + /* + * Load the song into the player. + */ LoadMODData(modData); return true; } @@ -174,21 +159,24 @@ public boolean LoadMODResource(int modresource) { /** * Stop playing the song, close down the player and * join() the player thread. - * - *

Typically called in the application's (Activity's) - * onPause() method. + *

Typically called in the application's (Activity's) + * onPause() method. */ public void StopAndClose() { PausePlay(); - boolean retry = true; - // Now close and join() the MOD player thread. StopThread(); + /* + * Now close and join() the MOD player thread. + */ + boolean retry = true; while (retry) { try { join(); retry = false; } catch (InterruptedException e) { - // Keep trying to close the player thread. + /* + * Keep trying to close the player thread. + */ } } CloseLIBMODPLUG(); diff --git a/src/com/peculiargames/andmodplug/PlayerThread.java b/src/com/peculiargames/andmodplug/PlayerThread.java index 83033fc..5942880 100644 --- a/src/com/peculiargames/andmodplug/PlayerThread.java +++ b/src/com/peculiargames/andmodplug/PlayerThread.java @@ -102,8 +102,7 @@ * Player class for MOD/XM song files (extends Java Thread). Has * methods to load song file data, play the song, get information about * the song, pause, unpause, etc. - *

- * Typical call order: + *

Typical call order: *
// get player instance (in topmost activity, etc.) *
pt = PlayerThread(); *
pt.LoadMODData(); // load MOD/XM data into player
@@ -119,109 +118,111 @@ *
pt.LoadMODData(newmodfiledata); *
pt.UnPausePlay(); *
// repeat... - * - * @version 1.0 - * - * @author P.A. Casey (crow) Peculiar-Games.com + * @version 1.0 + * @author P.A. Casey (crow) Peculiar-Games.com + * */ public class PlayerThread extends Thread { public final static String VERS = "1.0"; private final static String LOGPREFIX = "PLAYERTHREAD"; - // flags for pattern changes /* - * constant for setPatternLoopRange() calls - change to + * Constant for setPatternLoopRange() calls - change to * new pattern range immediately */ public final static int PATTERN_CHANGE_IMMEDIATE = 1; + /* - * constant for setPatternLoopRange() calls - change to + * Constant for setPatternLoopRange() calls - change to * new pattern range after currently playing pattern finishes */ public final static int PATTERN_CHANGE_AFTER_CURRENT = 2; + /* - * constant for setPatternLoopRange() calls - change to + * Constant for setPatternLoopRange() calls - change to * new pattern range after current range of patterns finishes playing */ public final static int PATTERN_CHANGE_AFTER_GROUP = 3; - // values for song loop counts + /* - * constant for setLoopCount() calls - loop song forever + * Constant for setLoopCount() calls - loop song forever */ public final static int LOOP_SONG_FOREVER = -1; - // - // Limit volume volume steps to 8 steps (just an arbitrary decision). - // There's also a setVolume() method that accepts a float. - // - // + /* + * Limit volume volume steps to 8 steps (just an arbitrary decision). + * There's also a setVolume() method that accepts a float. + */ public final static float[] sVolume_floats = {0.0f, 0.125f, 0.25f, 0.375f, 0.5f, 0.625f, 0.75f, 1.0f}; - // - // Object for lock on PlayerValid check (mostly necessary for passing - // a single PlayerThread instance among Activities in an Android - // multi-activity application). - // - // + /* + * Object for lock on PlayerValid check (mostly necessary for passing + * a single PlayerThread instance among Activities in an Android + * multi-activity application). + */ public static Object sPVlock; - // - // Object for lock on ReadData call (to prevent UI thread messing - // with player thread's GetSoundData() calls). - // - // + /* + * Object for lock on ReadData call (to prevent UI thread messing + * with player thread's GetSoundData() calls). + */ public static Object sRDlock; - // - // Mark the player as invalid for when an Activity shuts it down, but - // Android allows a reference to the player to persist. A better - // solution is probably to just null out the reference to the - // PlayerThread object in whichever Activity shuts it down. - // - // + /* + * Mark the player as invalid for when an Activity shuts it down, but + * Android allows a reference to the player to persist. A better + * solution is probably to just null out the reference to the + * PlayerThread object in whichever Activity shuts it down. + */ public boolean mPlayerValid = false; private boolean mWaitFlag = false; private boolean mFlushedData = false; private boolean mPlaying = true; private boolean mRunning = true; - // - // Android will report the minimum buffer size needed to keep playing - // audio at our requested rate smoothly. - // - // + /* + * Android will report the minimum buffer size needed to keep playing + * audio at our requested rate smoothly. + */ private int mMinbuffer; private int mModsize; // holds the size in bytes of the mod file private final static int BUFFERSIZE = 20000; // the sample buffer size - private AudioTrack mMytrack; private boolean mLoad_ok; - // for storing info about the MOD file currently loaded + /* + * Variables for storing info about the MOD file currently loaded. + */ private String mModname; private int mNumChannels; private int mRate; private int posWas; private boolean songFinished; - // Track if player has started (after loading a new mod). + /* + * Track if player has started (after loading a new mod). + */ private boolean sPlayerStarted; - // start the player in a paused state? + /* + * Start the player in a paused state? + */ private boolean mStart_paused; + /* + * Audio sampling rate definitions. + */ private static final int NUM_RATES = 5; private final int[] try_rates = {44100, 32000, 22000, 16000, 8000}; - // - // Ownership code -- for when several activities try to share a - // single mod player instance... - // - // This probably needs to be synchronized... - // - // + /* + * Ownership code -- for when several activities try to share a + * single mod player instance... + * + * This probably needs to be synchronized... + */ private Object mOwner; public boolean TakeOwnership(Object newowner) { @@ -246,17 +247,26 @@ public Object GetOwner() { return mOwner; } - //******************************************************************* + //******************************************************************** // Listener interface for various events - //******************************************************************* - // Event types. - public static final int EVENT_PLAYER_STARTED = 1; - public static final int EVENT_PATTERN_CHANGE = 2; - public static final int EVENT_SONG_COMPLETED = 3; + //******************************************************************** - // Listener user set. + /* + * Event types enumeration. + */ + public static enum eventEnum { + PLAYER_STARTED, + PATTERN_CHANGE, + SONG_COMPLETED; + } + + /** + * Music player event listener set. + * @author P.A. Casey (crow) Peculiar-Games.com + * + */ public interface PlayerListener { - public abstract void onPlayerEvent(int type); + public abstract void onPlayerEvent(eventEnum event); } private PlayerListener mPlayerListener = null; @@ -265,48 +275,44 @@ public void setPlayerListener(PlayerListener pl) { mPlayerListener = pl; } - //******************************************************************* - // Constructors - //******************************************************************* - // - // Here's (one of) the constructor(s) -- grabs an audio track and - // loads a mod file - // - // MOD file data has already been read in (using a FileStream) by the - // caller -- that functionality could probably be included here, but - // for now we'll do it this way. - // - // You could use this in the top parent activity (like a game menu) - // to create a PlayerThread and load the mod data in one call. - // - // + /* + * Here's (one of) the constructor(s) - grabs an audio track and loads + * a MOD file. + * + * MOD file data has already been read in (using a FileStream) by the + * caller -- that functionality could probably be included here, but + * for now we'll do it this way. + * + * You could use this in the top parent activity (like a game menu) + * to create a PlayerThread and load the mod data in one call. + */ + /** * Allocates a MOD/XM/etc. song PlayerThread - * *

The modData argument is a byte[] array with the MOD file * preloaded into it. The desiredrate argument is a specifier that * attempts to set the rate audio data will play at - will be * overridden if the OS doesn't allow that rate. - * - * @param modData - * - A byte[] array containing the MOD file data. - * - * @param desiredrate - * - Rate of playback (e.g. 44100Hz, or 0 for default rate) - * for system audio data playback. + * @param modData - A byte[] array containing the MOD file data. + * @param desiredrate - Rate of playback (e.g. 44100Hz, or 0 for + * default rate) for system audio data playback. */ public PlayerThread(byte[] modData, int desiredrate) { /* * Just call the regular constructor and then load in the supplied * MOD file data. - */ + */ this(desiredrate); - // load the mod file (data) into libmodplug + /* + * Load the mod file (data) into libmodplug. + */ mLoad_ok = ModPlug_JLoad(modData, modData.length); if (mLoad_ok) { - // get info (name and number of tracks) for the loaded MOD file + /* + * Get info (name and number of tracks) for the loaded MOD file. + */ mModname = ModPlug_JGetName(); mNumChannels = ModPlug_JNumChannels(); posWas = 0; @@ -318,19 +324,15 @@ public PlayerThread(byte[] modData, int desiredrate) { * Allocates a MOD/XM/etc. song PlayerThread. This method just gets * an audio track. The mod file will be loaded later with a call to * LoadMODData(). - *

- * The desiredrate argument is a specifier that attempts to set the + *

The desiredrate argument is a specifier that attempts to set the * rate audio data will play at - will be overridden if the OS doesn't * allow that rate. - *

- * General call order when using this constructor is: + *

General call order when using this constructor is: *
pthr = new PlayerThread(0); *
pthr.LoadMODData(modData); *
pthr.start();
- * - * @param desiredrate - * - Rate of playback (e.g. 44100Hz, or 0 for default rate) - * for system audio data playback. + * @param desiredrate - Rate of playback (e.g. 44100Hz, or 0 for + * default rate) for system audio data playback. */ public PlayerThread(int desiredrate) { // no Activity owns this player yet @@ -353,18 +355,17 @@ public PlayerThread(int desiredrate) { private boolean GetAndroidAudioTrack(int desiredrate) { int rateindex = 0; - // - // Get a stereo audio track from Android. - // - // PACKETSIZE is the amount of data we request from libmodplug, - // minbuffer is the size Android tells us is necessary to play - // smoothly for the rate, configuration we want and is a separate - // buffer the OS handles. - // - // Init the track and player for the desired rate, or if none - // specified, highest possible. - // - // + /* + * Get a stereo audio track from Android. + * + * PACKETSIZE is the amount of data we request from libmodplug, + * minbuffer is the size Android tells us is necessary to play + * smoothly for the rate, configuration we want and is a separate + * buffer the OS handles. + * + * Init the track and player for the desired rate, or if none + * specified, highest possible. + */ if (desiredrate == 0) { boolean success = false; while (!success && (rateindex < NUM_RATES)) { @@ -377,7 +378,9 @@ private boolean GetAndroidAudioTrack(int desiredrate) { AudioFormat.CHANNEL_CONFIGURATION_STEREO, AudioFormat.ENCODING_PCM_16BIT, mMinbuffer, AudioTrack.MODE_STREAM); - // init the Modplug player for this sample rate + /* + * Init the Modplug player for this sample rate. + */ ModPlug_Init(try_rates[rateindex]); success = true; } catch (IllegalArgumentException e) { @@ -393,7 +396,9 @@ private boolean GetAndroidAudioTrack(int desiredrate) { AudioFormat.CHANNEL_CONFIGURATION_STEREO, AudioFormat.ENCODING_PCM_16BIT, mMinbuffer, AudioTrack.MODE_STREAM); - // init the Modplug player for this sample rate + /* + * Init the Modplug player for this sample rate. + */ ModPlug_Init(desiredrate); } @@ -404,7 +409,9 @@ private boolean GetAndroidAudioTrack(int desiredrate) { if (mMytrack == null) { mPlayerValid = false; - // couldn't get an audio track so return false to caller + /* + * Couldn't get an audio track so return false to caller. + */ return false; } else { @@ -425,18 +432,18 @@ private boolean GetAndroidAudioTrack(int desiredrate) { break; } } - // got the audio track! + /* + * Got the audio track! + */ return true; } /** * Loads MOD/XM,etc. song data for playback. Call PausePlay() if a * song is currently playing prior to invoking this method. - *

- * The modData argument is a byte[] array with the MOD containing the - * song file data. - *

- * Example of loading the data:
+ *

The modData argument is a byte[] array with the MOD containing + * the song file data. + *

Example of loading the data:
* modfileInStream = getResources().openRawResource(R.raw.coolxmsong); *
try {
* modsize = modfileInStream.read(modData,0, @@ -444,9 +451,7 @@ private boolean GetAndroidAudioTrack(int desiredrate) { *
} catch (IOException e) { *
e.printStackTrace(); *
}
- * - * @param modData - * - A byte[] array containing the MOD file data. + * @param modData - A byte[] array containing the MOD file data. */ public void LoadMODData(byte[] modData) { UnLoadMod(); @@ -459,12 +464,11 @@ public void LoadMODData(byte[] modData) { songFinished = false; } - // - // Re-init this flag so that an event will be passed to the - // PlayerListener after the first write() to the AudioTrack - when - // I assume music will actually start playing... - // - // + /* + * Re-init this flag so that an event will be passed to the + * PlayerListener after the first write() to the AudioTrack - when + * I assume music will actually start playing... + */ synchronized(this) { sPlayerStarted = false; } @@ -473,13 +477,11 @@ public void LoadMODData(byte[] modData) { /** * This PlayerValid stuff is for multi-activity use, or also * Android's Pause/Resume. - *

- * A better way to deal with it is probably to always stop and + *

A better way to deal with it is probably to always stop and * join() the PlayerThread in onPause() and * allocate a new PlayerThread in onResume() (or * onCreate()??). - *

- * Check if the player thread is still valid. + *

Check if the player thread is still valid. */ public boolean PlayerValid() { // return whether this player is valid @@ -500,8 +502,7 @@ public void InvalidatePlayer() { /** * The thread's run() call, where the modules are played. - *

- * Start playing the MOD/XM song (hopefully it's been previously + *

Start playing the MOD/XM song (hopefully it's been previously * loaded using LoadMODData() or * LoadMODResource() ;) */ @@ -522,7 +523,9 @@ public void run() { else mPlaying = true; - // main play loop + /* + * Main play loop. + */ if (mMytrack != null) mMytrack.play(); else @@ -530,7 +533,9 @@ public void run() { while (mRunning) { while (mPlaying) { - // pre-load another packet + /* + * Pre-load another packet. + */ synchronized(sRDlock) { ModPlug_JGetSoundData(mBuffer, BUFFERSIZE); @@ -538,22 +543,20 @@ public void run() { pattern_change = true; } - // - // Pass a packet of sound sample data to the audio track - // (blocks until audio track can accept the new data). - // - // + /* + * Pass a packet of sound sample data to the audio track + * (blocks until audio track can accept the new data). + */ mMytrack.write(mBuffer, 0, BUFFERSIZE); - // - // Send player events. - // - // + /* + * Send player events. + */ synchronized(this) { if (!sPlayerStarted) { sPlayerStarted = true; if (mPlayerListener != null) { - mPlayerListener.onPlayerEvent(EVENT_PLAYER_STARTED); + mPlayerListener.onPlayerEvent(eventEnum.PLAYER_STARTED); } } } @@ -563,7 +566,7 @@ public void run() { pattern_change = false; if (mPlayerListener != null) - mPlayerListener.onPlayerEvent(EVENT_PATTERN_CHANGE); + mPlayerListener.onPlayerEvent(eventEnum.PATTERN_CHANGE); } } @@ -575,7 +578,7 @@ public void run() { if (!songFinished && ((posNow < posWas) || (posNow >= getMaxPos()))) { if (mPlayerListener != null) - mPlayerListener.onPlayerEvent(EVENT_SONG_COMPLETED); + mPlayerListener.onPlayerEvent(eventEnum.SONG_COMPLETED); songFinished = true; } @@ -600,10 +603,14 @@ public void run() { } } } - // Clear flushed flag. + /* + * Clear flushed flag. + */ mFlushedData = false; } - // Release the audio track resources. + /* + * Release the audio track resources. + */ if (mMytrack != null) { mMytrack.release(); @@ -617,7 +624,6 @@ public void run() { /** * Get the name of the song. - * * @return the name of the song (from the MOD/XM file header) */ public String getModName() { @@ -628,7 +634,6 @@ public String getModName() { * Get the number of channels used in the song (MOD/XM songs * typically use from 4 to 32 channels in a pattern, mixed together * for awesomeness). - * * @return the number of channels the song uses */ public int getNumChannels() { @@ -644,7 +649,6 @@ public void setModSize(int modsize) { /** * Get the file size of the MOD/XM song. - * * @return the size of the song file */ public int getModSize() { @@ -705,9 +709,7 @@ public void Flush() { /** * Sets playback volume for the MOD/XM player. - * - * @param vol - * - An integer from 0 (sound off) to 255 (full volume). + * @param vol - An integer from 0 (sound off) to 255 (full volume). */ public void setVolume(int vol) { vol = vol>>5; @@ -718,10 +720,8 @@ public void setVolume(int vol) { /** * Sets playback volume for the MOD/XM player. - * - * @param vol - * - A floating point number from 0.0f (sound off) to 1.0f - * (full volume). + * @param vol - A floating point number from 0.0f (sound off) to 1.0f + * (full volume). */ public void setVolume(float vol) { if (vol>1.0f) vol = 1.0f; @@ -731,12 +731,10 @@ public void setVolume(float vol) { /** * This method sets the player startup mode. - * - * @param flag - * - If this is set to true, then the player will start up - * paused and will have to be unpaused to start playing. If - * this is set to false, then the player will immediately - * begin playing when it is started. + * @param flag - If this is set to true, then the player will start up + * paused and will have to be unpaused to start playing. If this is + * set to false, then the player will immediately begin playing when + * it is started. */ public void startPaused(boolean flag) { /* @@ -749,14 +747,15 @@ public void startPaused(boolean flag) { /** * This completely stops the thread, which will also stop the current * song if it is playing. - *

- * Typically the player should then be join()ed to + *

Typically the player should then be join()ed to * completely remove the thread from the application's Android * process, and also call CloseLIBMODPLUG() to close * the native player library and de-allocate all resources it used. */ public void StopThread() { - // Stops the music player thread (see run() above). + /* + * Stops the music player thread (see run() above). + */ mPlaying = false; mRunning = false; /* @@ -764,8 +763,12 @@ public void StopThread() { * track, but seem to get an uninitialized audio track here * occasionally, generating an IllegalStateException. */ - if (mMytrack.getState() == AudioTrack.STATE_INITIALIZED) - mMytrack.stop(); + try { + if (mMytrack.getState() == AudioTrack.STATE_INITIALIZED) + mMytrack.stop(); + } catch (IllegalStateException ise) { + ise.printStackTrace(); + } mPlayerValid = false; mWaitFlag = false; @@ -776,8 +779,8 @@ public void StopThread() { } /** - * Close the native internal tracker library (libmodplug) and de- - * allocate any resources. + * Close the native internal tracker library (libmodplug) and + * deallocate any resources. */ public void CloseLIBMODPLUG() { ModPlug_JUnload(); @@ -793,10 +796,8 @@ public void CloseLIBMODPLUG() { /** * EXPERIMENTAL method for modifying the song's tempo (+ or -) by * mt. - * - * @param mt - * - Modifier for the song's "native" tempo (positive values - * to increase tempo, negative values to decrease tempo). + * @param mt - Modifier for the song's "native" tempo (positive values + * to increase tempo, negative values to decrease tempo). */ public void modifyTempo(int mt) { ModPlug_ChangeTempo(mt); @@ -804,49 +805,43 @@ public void modifyTempo(int mt) { /** * EXPERIMENTAL method for setting the song's tempo to * tempo. - * - * @param tempo - * - The tempo for the song (overrides song's "native" tempo). + * @param tempo - The tempo for the song (overrides song's "native" + * tempo). */ public void setTempo(int tempo) { ModPlug_SetTempo(tempo); } /** * EXPERIMENTAL: Get the default tempo from the song's header. - * - * @return the tempo + * @return the tempo. */ public int getSongDefaultTempo() { return ModPlug_GetNativeTempo(); } /** * EXPERIMENTAL: Get the current "position" in song - * - * @return the position + * @return the position. */ public int getCurrentPos() { return ModPlug_GetCurrentPos(); } /** * EXPERIMENTAL: Get the maximum "position" in song - * - * @return the maximum position + * @return the maximum position. */ public int getMaxPos() { return ModPlug_GetMaxPos(); } /** * EXPERIMENTAL: Get the current order - * - * @return the order + * @return the order. */ public int getCurrentOrder() { return ModPlug_GetCurrentOrder(); } /** * EXPERIMENTAL: Get the current pattern - * - * @return the pattern + * @return the pattern. */ public int getCurrentPattern() { return ModPlug_GetCurrentPattern(); @@ -854,9 +849,7 @@ public int getCurrentPattern() { /** * EXPERIMENTAL: set the current pattern (pattern is changed but * plays from current row in pattern). - * - * @param pattern - * - The new pattern to start playing immediately. + * @param pattern - The new pattern to start playing immediately. */ public void setCurrentPattern(int pattern) { ModPlug_SetCurrentPattern(pattern); @@ -864,28 +857,23 @@ public void setCurrentPattern(int pattern) { /** * EXPERIMENTAL: set the next pattern to play after current pattern * finishes. - * - * @param pattern - * - The new pattern to start playing after the current - * pattern finishes playing. + * @param pattern - The new pattern to start playing after the current + * pattern finishes playing. */ public void setNextPattern(int pattern) { ModPlug_SetNextPattern(pattern); } /** * EXPERIMENTAL: Get the current row in the pattern - * - * @return the row + * @return the row. */ public int getCurrentRow() { return ModPlug_GetCurrentRow(); } /** * EXPERIMENTAL: Set log printing flag - * - * @param flag - * - True to start printing debug information to log output, - * false to stop. + * @param flag - true to start printing debug information + * to log output, false to stop. */ public void setLogOutput(boolean flag) { ModPlug_LogOutput(flag); @@ -894,10 +882,8 @@ public void setLogOutput(boolean flag) { * EXPERIMENTAL method to change patterns in a song (playing in * PATTERN LOOP mode). Waits for the currently playing pattern to * finish. - * - * @param newpattern - * - The new song pattern to start playing (repeating) in - * PATTERN LOOP mode. + * @param newpattern - The new song pattern to start playing + * (repeating) in PATTERN LOOP mode. */ public void changePattern(int newpattern) { ModPlug_ChangePattern(newpattern); @@ -905,34 +891,26 @@ public void changePattern(int newpattern) { /** * EXPERIMENTAL method to change song to PATTERN LOOP mode, repeating * pattern - * - * @param pattern - * - The song pattern to start playing(repeating) in PATTERN - * LOOP mode. + * @param pattern - The song pattern to start playing(repeating) in + * PATTERN LOOP mode. */ public void repeatPattern(int pattern) { ModPlug_RepeatPattern(pattern); } /** * EXPERIMENTAL method to loop song in a group of patterns. - * - * @param from - * - Start of pattern range to play in loop. - * @param to - * - End of pattern range to play in loop. - * @param when - * - A constant flag (PATTERN_CHANGE_IMMEDIATE, - * PATTERN_CHANGE_AFTER_CURRENT, PATTERN_CHANGE_AFTER_GROUP) - * to signal when the new pattern range should take effect. + * @param from - Start of pattern range to play in loop. + * @param to - End of pattern range to play in loop. + * @param when - A constant flag (PATTERN_CHANGE_IMMEDIATE, + * PATTERN_CHANGE_AFTER_CURRENT, PATTERN_CHANGE_AFTER_GROUP) to signal + * when the new pattern range should take effect. */ public void setPatternLoopRange(int from, int to, int when) { ModPlug_SetPatternLoopRange(from, to, when); } /** * EXPERIMENTAL method to loop song the specified number of times. - * - * @param number - * - The number of times to loop (-1 = forever). + * @param number - The number of times to loop (-1 = forever). */ public void setLoopCount(int loopcount) { ModPlug_SetLoopCount(loopcount); @@ -941,10 +919,8 @@ public void setLoopCount(int loopcount) { * EXPERIMENTAL method to set song to PATTERN LOOP mode, repeating * any pattern playing or subsequently set via * changePattern(). - * - * @param flag - * - True to set PATTERN LOOP mode, false to turn off PATTERN - * LOOP mode. + * @param flag - true to set PATTERN LOOP mode, + * false to turn off PATTERN LOOP mode. */ public void setPatternLoopMode(boolean flag) { ModPlug_SetPatternLoopMode(flag); @@ -952,27 +928,24 @@ public void setPatternLoopMode(boolean flag) { /** * Unload the current mod from libmodplug, but make sure to wait * until any GetSoundData() call in the player thread has finished. - *

- * Unload MOD/XM data previously loaded into the native player + *

Unload MOD/XM data previously loaded into the native player * library. */ public void UnLoadMod() { - // - // Since this can/will be called from the UI thread, need to synch - // and not have a call into libmodplug unloading the file, while a - // call to GetModData() is also executing in the player thread (see - // run() above). - // - // + /* + * Since this can/will be called from the UI thread, need to synch + * and not have a call into libmodplug unloading the file, while a + * call to GetModData() is also executing in the player thread (see + * run() above). + */ synchronized(sRDlock) { ModPlug_JUnload(); } } - // - // Native methods in our JNI libmodplug stub code. - // - // + /* + * Native methods in our JNI libmodplug stub code. + */ public native boolean ModPlug_Init(int rate); public native boolean ModPlug_JLoad(byte[] buffer, int size); public native String ModPlug_JGetName(); @@ -981,7 +954,9 @@ public void UnLoadMod() { public native boolean ModPlug_JUnload(); public native boolean ModPlug_CloseDown(); - // HACKS ;-) + /* + * HACKS ;-). + */ public native int ModPlug_GetNativeTempo(); public native void ModPlug_ChangeTempo(int tempotweak); public native void ModPlug_SetTempo(int tempo); @@ -991,21 +966,21 @@ public void UnLoadMod() { public native void ModPlug_SetPatternLoopMode(boolean flag); public native void ModPlug_SetCurrentPattern(int pattern); public native void ModPlug_SetNextPattern(int pattern); + public native void ModPlug_SetPatternLoopRange(int from, int to, int when); + public native void ModPlug_SetLoopCount(int loopcount); - // More info + /* + * More info. + */ public native int ModPlug_GetCurrentPos(); public native int ModPlug_GetMaxPos(); public native int ModPlug_GetCurrentOrder(); public native int ModPlug_GetCurrentPattern(); public native int ModPlug_GetCurrentRow(); - // FOURBYFOUR - public native void ModPlug_SetPatternLoopRange(int from, - int to, - int when); - public native void ModPlug_SetLoopCount(int loopcount); - - // Log output + /* + * Log output. + */ public native void ModPlug_LogOutput(boolean flag); static { @@ -1017,11 +992,10 @@ public native void ModPlug_SetPatternLoopRange(int from, Log.e(LOGPREFIX, "------ older or differently named libmodplug???"); } - // - // Get lock objects for synchronizing access to playervalid flag and - // GetSoundData() call. - // - // + /* + * Get lock objects for synchronizing access to playervalid flag and + * GetSoundData() call. + */ sPVlock = new Object(); sRDlock = new Object(); } diff --git a/src/org/gsanson/frozenbubble/Freile.java b/src/org/gsanson/frozenbubble/Freile.java index bf1b1ce..5b175ee 100644 --- a/src/org/gsanson/frozenbubble/Freile.java +++ b/src/org/gsanson/frozenbubble/Freile.java @@ -83,14 +83,24 @@ public class Freile implements Opponent, Runnable { {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}; - //********************************************************** + //******************************************************************** // Listener interface for various opponent events - //********************************************************** - // Event types. - public static final int EVENT_DONE_COMPUTING = 1; - // Listener user set. + //******************************************************************** + + /* + * Event types enumeration. + */ + public static enum eventEnum { + DONE_COMPUTING; + } + + /** + * Opponent event listener user set. + * @author Glenn Sanson + * + */ public interface OpponentListener { - public abstract void onOpponentEvent(int event); + public abstract void onOpponentEvent(eventEnum event); } OpponentListener mOpponentListener; @@ -99,23 +109,23 @@ public void setOpponentListener (OpponentListener ol) { mOpponentListener = ol; } - /** Reference to the managed game grid */ + /* Reference to the managed game grid */ private BubbleSprite[][] grid; - /** Current color */ + /* Current color */ private int color; - /** Next color */ + /* Next color */ private int nextColor; - /** Current compressor level */ + /* Current compressor level */ private int compressor; - /** Swap launch bubble with next bubble? */ + /* Swap launch bubble with next bubble? */ private boolean colorSwap; - /** Calculating new position */ + /* Calculating new position */ private boolean computing; - /** Thread running flag */ + /* Thread running flag */ private boolean running; - /** Best direction */ + /* Best direction */ private double bestDirection; - /** Best ball destination */ + /* Best ball destination */ private int[] bestDestination; public Freile(BubbleSprite[][] grid) { @@ -128,7 +138,6 @@ public Freile(BubbleSprite[][] grid) { /** * Checks if work is still in progress. - * * @return true if the calculation is not yet finished */ public boolean isComputing() { @@ -136,7 +145,9 @@ public boolean isComputing() { } public double getExactDirection(double currentDirection) { - // currentDirection is not used there + /* + * currentDirection is not used here. + */ return bestDirection; } @@ -189,7 +200,7 @@ public void run() { if (computing) { computing = false; if (mOpponentListener != null) - mOpponentListener.onOpponentEvent(EVENT_DONE_COMPUTING); + mOpponentListener.onOpponentEvent(eventEnum.DONE_COMPUTING); } while (running && !computing) { @@ -204,10 +215,14 @@ public void run() { } if (running) { - // Compute grid options + /* + * Compute grid options. + */ int[][] gridOptions = new int[8][13]; - // Check for best option + /* + * Check for best option. + */ int bestOption = -1; bestDirection = 0.; colorSwap = false; @@ -325,7 +340,9 @@ private int[] getCollision(double direction) { speedX = -speedX; } - // Check top collision + /* + * Check top collision. + */ if (posY < 0.) { int valX = (int) posX; @@ -336,7 +353,9 @@ private int[] getCollision(double direction) { } position[1] = 0; } else { - // Check other collision + /* + * Check other collision. + */ position = CollisionHelper.collide((int) posX, (int) posY, grid); } } @@ -346,8 +365,8 @@ private int[] getCollision(double direction) { /** * Stop the thread run() execution. - *

- * Interrupt the thread when it is suspended via wait(). + *

Interrupt the thread when it is suspended via + * wait(). */ public void stopThread() { running = false; diff --git a/src/org/gsanson/frozenbubble/MalusBar.java b/src/org/gsanson/frozenbubble/MalusBar.java index 40dbbdb..93a3519 100644 --- a/src/org/gsanson/frozenbubble/MalusBar.java +++ b/src/org/gsanson/frozenbubble/MalusBar.java @@ -61,36 +61,30 @@ public class MalusBar extends Sprite { - /** X-pos for tomatoes */ + /* X-pos for tomatoes */ int minX; - /** Max Y-pos for bar */ + /* Max Y-pos for bar */ int maxY; - /** Number of waiting bubbles */ + /* Number of waiting bubbles */ int nbMalus; - /** Time to release bubbles */ + /* Time to release bubbles */ public int releaseTime; - - /** Banana Image */ + /* Banana Image */ private BmpWrap banana; - /** Tomato Image */ + /* Tomato Image */ private BmpWrap tomato; + /* Attack bubble array */ + public byte[] attackBubbles = { -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1 }; /** * Manages a malus bar (bananas & tomatoes). - * - * @param coordX - * - X-coord of game facade. - * @param coordY - * - Y-coord of game facade. - * - * @param leftSide - * - if on left side (false => right side). - * - * @param tomato - * - image resource for a tomato. - * - * @param banana - * - image resource for a banana. + * @param coordX - X-coord of game facade. + * @param coordY - Y-coord of game facade. + * @param leftSide - if on left side (false => right side). + * @param tomato - image resource for a tomato. + * @param banana - image resource for a banana. */ public MalusBar(int coordX, int coordY, BmpWrap banana, BmpWrap tomato) { super(new Rect(coordX, coordY, coordX + 33, coordY + 354)); @@ -119,12 +113,20 @@ public final void paint(Canvas c, double scale, int dx, int dy) { } public void addBubbles(int toAdd) { - if (toAdd > 0) + if ((toAdd > 0) && (nbMalus == 0)) releaseTime = 0; nbMalus += toAdd; } - public int getBubbles() { + /** + * Clear the attack bubbles stored in the attack bubble array. + */ + public void clearAttackBubbles() { + for (int i = 0; i < 15; i++) + this.attackBubbles[i] = -1; + } + + public int getAttackBarBubbles() { return nbMalus; } @@ -132,6 +134,20 @@ public int getTypeId() { return Sprite.TYPE_IMAGE; } + /** + * The number of attack bubbles will be decremented by the supplied + * number of bubbles, which is the number of attack bubbles that were + * previously launched. + * @param remove - the number of attack bubbles to remove from the + * total number of attack bubbles. + */ + public void removeAttackBubbles(int remove) { + nbMalus -= remove; + + if (nbMalus < 0) + nbMalus = 0; + } + public int removeLine() { int nb = Math.min(7, nbMalus); nbMalus -= nb; @@ -147,4 +163,27 @@ public void saveState(Bundle map, int id) { map.putInt(String.format("%d-nbMalus", id), nbMalus); map.putInt(String.format("%d-releaseTime", id), releaseTime); } + + /** + * Set the value of an attack bubble color in the attack bubble array. + * @param bubbleIndex - the update index in the attack bubble array. + * @param bubbleColor - the attack bubble color. + */ + public void setAttackBubble(int bubbleIndex, int bubbleColor) { + this.attackBubbles[bubbleIndex] = (byte) bubbleColor; + } + + /** + * Set the total number of attack bubbles stored in the attack bar, + * as well as the array of current attack bubbles. + * @param numBubbles - the total number of attack bubbles. + * @param attackBubbles - the array of attack bubbles. + */ + public void setAttackBubbles(int numBubbles, byte[] attackBubbles) { + nbMalus = numBubbles; + + if (attackBubbles != null) + for (int i = 0; i < 15; i++) + this.attackBubbles[i] = attackBubbles[i]; + } } diff --git a/src/org/jfedor/frozenbubble/BubbleFont.java b/src/org/jfedor/frozenbubble/BubbleFont.java index e44ae18..05247ed 100644 --- a/src/org/jfedor/frozenbubble/BubbleFont.java +++ b/src/org/jfedor/frozenbubble/BubbleFont.java @@ -63,7 +63,7 @@ public class BubbleFont { '?', '@', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '{', - '}', '[', ']', ' ', '\\', ' ', ' '}; + '}', '[', ']', '^', '\\', '_', '~'}; private int[] position = { 0, 9, 16, 31, 39, 54, 69, 73, 80, 88, 96, 116, 121, 131, diff --git a/src/org/jfedor/frozenbubble/BubbleSprite.java b/src/org/jfedor/frozenbubble/BubbleSprite.java index 687357d..4379948 100644 --- a/src/org/jfedor/frozenbubble/BubbleSprite.java +++ b/src/org/jfedor/frozenbubble/BubbleSprite.java @@ -208,7 +208,7 @@ boolean checkCollision(BubbleSprite sprite) { (sprite.getSpriteArea().top - this.realY) * (sprite.getSpriteArea().top - this.realY); - return (value < minDistance); + return value < minDistance; } public boolean checked() { @@ -477,8 +477,7 @@ else if (realX<=190.) { if (checkJump.size() >= 3) { released = true; - frozen.setSendToOpponent(frozen.getSendToOpponent() + - checkJump.size() - 3); + frozen.addAttackBubbles(checkJump.size() - 3); for (int i=0 ; i width)) || - (((rotation == Surface.ROTATION_90 ) || - (rotation == Surface.ROTATION_270)) && (width > height))) { - // - // Natural orientation is portrait. - // - // - switch(rotation) { - case Surface.ROTATION_0: - orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - break; - case Surface.ROTATION_90: - orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; - break; - case Surface.ROTATION_180: - orientation = SCREEN_ORIENTATION_REVERSE_PORTRAIT; - break; - case Surface.ROTATION_270: - orientation = SCREEN_ORIENTATION_REVERSE_LANDSCAPE; - break; - default: - orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - break; - } - } - else { - // - // Natural orientation is landscape or square. - // - // - switch(rotation) { - case Surface.ROTATION_0: - orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; - break; - case Surface.ROTATION_90: - orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - break; - case Surface.ROTATION_180: - orientation = SCREEN_ORIENTATION_REVERSE_LANDSCAPE; - break; - case Surface.ROTATION_270: - orientation = SCREEN_ORIENTATION_REVERSE_PORTRAIT; - break; - default: - orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; - break; - } - } - - return orientation; + public synchronized static void setAdsOn(boolean newAdsOn) { + adsOn = newAdsOn; } - - private void newGameDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(FrozenBubble.this); - // - // Set the dialog title. - // - // - builder.setTitle(R.string.menu_new_game) - // Set the action buttons - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - // User clicked OK. Start a new game. - newGame(); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - // User clicked Cancel. Do nothing. - } - }); - builder.create(); - builder.show(); + public synchronized static boolean getAimThenShoot() { + return (targetMode == AIM_TO_SHOOT) || (targetMode == ROTATE_TO_SHOOT); } - /** - * Method to start a game using levels from the level editor. - *

- * If the level isn't specified from the editor, then the player - * selected the option to continue playing from the last level - * played, so use the last level played instead. - * - * @param intent - * - The intent from the level editor used to start this - * activity, which contains the custom level data. - */ - private void startCustomGame(Intent intent) { - activityCustomStarted = true; - numPlayers = 1; - // Get custom level last played. - SharedPreferences sp = getSharedPreferences(PREFS_NAME, - Context.MODE_PRIVATE); - int startingLevel = sp .getInt ("levelCustom", 0); - int startingLevelIntent = intent.getIntExtra("startingLevel", -2); - startingLevel = - (startingLevelIntent == -2) ? startingLevel : startingLevelIntent; - mGameView = new GameView(this, - intent.getExtras().getByteArray("levels"), - startingLevel); - setContentView(mGameView); - mGameView.setGameListener(this); - mGameThread = mGameView.getThread(); - mGameView.requestFocus(); - setFullscreen(); - playMusic(false); - } - - /** - * Method to start a game using default levels, if puzzle game mode - * was selected. - * - * @param intent - * - The intent used to start this activity. - * @param savedInstanceState - * - the bundle of saved state information. - */ - private void startDefaultGame(Intent intent, Bundle savedInstanceState) { - // Default levels. - activityCustomStarted = false; - // Check if this is a single player or multiplayer game. - numPlayers = 1; - if (intent != null) { - if (intent.hasExtra("numPlayers")) - numPlayers = intent.getIntExtra("numPlayers", 1); - } - - if (numPlayers > 1) { - mMultiplayerGameView = new MultiplayerGameView(this, numPlayers); - setContentView(mMultiplayerGameView); - mMultiplayerGameView.setGameListener(this); - mMultiplayerGameThread = mMultiplayerGameView.getThread(); - if (savedInstanceState != null) { - int savedPlayers = savedInstanceState.getInt("numPlayers"); - if (savedPlayers == 2) - mMultiplayerGameThread.restoreState(savedInstanceState); - } - mMultiplayerGameThread.startOpponent(); - mMultiplayerGameView.requestFocus(); - } - else { - setContentView(R.layout.activity_frozen_bubble); - mGameView = (GameView)findViewById(R.id.game); - mGameView.setGameListener(this); - mGameThread = mGameView.getThread(); - if (savedInstanceState != null) { - int savedPlayers = savedInstanceState.getInt("numPlayers"); - if (savedPlayers == 1) - mGameThread.restoreState(savedInstanceState); - } - - mGameView.requestFocus(); - } - setFullscreen(); - playMusic(false); - } - - private void setFullscreen() { - final int flagFs = WindowManager.LayoutParams.FLAG_FULLSCREEN; - final int flagNoFs = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; - - if (fullscreen) { - getWindow().addFlags(flagFs); - getWindow().clearFlags(flagNoFs); - } - else { - getWindow().clearFlags(flagFs); - getWindow().addFlags(flagNoFs); - } - - if (mGameView != null) - mGameView.requestLayout(); - - if (mMultiplayerGameView != null) - mMultiplayerGameView.requestLayout(); - } - - private void soundOptionsDialog() { - boolean isCheckedItem[] = {getSoundOn(), getMusicOn()}; - - AlertDialog.Builder builder = new AlertDialog.Builder(FrozenBubble.this); - // - // Set the dialog title. - // - // - builder.setTitle(R.string.menu_sound_options) - // - // Specify the list array, the items to be selected by default - // (null for none), and the listener through which to receive - // callbacks when items are selected. - // - // - .setMultiChoiceItems(R.array.sound_options_array, isCheckedItem, - new DialogInterface.OnMultiChoiceClickListener() { - @Override - public void onClick(DialogInterface dialog, int which, boolean isChecked) { - SharedPreferences sp = getSharedPreferences(PREFS_NAME, - Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - - switch (which) { - case 0: - setSoundOn(isChecked); - editor.putBoolean("soundOn", soundOn); - editor.commit(); - break; - case 1: - setMusicOn(isChecked); - if (myModPlayer != null) { - myModPlayer.setMusicOn(isChecked); - } - editor.putBoolean("musicOn", musicOn); - editor.commit(); - break; - } - } - }) - // Set the action buttons - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - // User clicked OK. - } - }); - builder.create(); - builder.show(); - } - - private void targetOptionsDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(FrozenBubble.this); - // - // Set the dialog title. - // - // - builder.setTitle(R.string.menu_target_mode) - // - // Specify the list array, the item to be selected by default, - // and the listener through which to receive callbacks when the - // item is selected. - // - // - .setSingleChoiceItems(R.array.shoot_mode_array, targetMode, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface builder, int which) { - switch (which) { - case 0: - setTargetMode(AIM_TO_SHOOT); - break; - case 1: - setTargetMode(POINT_TO_SHOOT); - break; - case 2: - setTargetMode(ROTATE_TO_SHOOT); - break; - } - setTargetModeOrientation(); - } - }) - // Set the action buttons - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface builder, int id) { - // User clicked OK. - SharedPreferences sp = getSharedPreferences(PREFS_NAME, - Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putInt("targetMode", targetMode); - editor.commit(); - } - }); - - builder.create(); - builder.show(); - } - - public synchronized static boolean getAdsOn() { - return adsOn; - } - - public synchronized static void setAdsOn(boolean newAdsOn) { - adsOn = newAdsOn; - } - - public synchronized static boolean getAimThenShoot() { - return ((targetMode == AIM_TO_SHOOT) || (targetMode == ROTATE_TO_SHOOT)); - } - - public synchronized static int getCollision() { - return collision; + public synchronized static int getCollision() { + return collision; } public synchronized static void setCollision(int newCollision) { @@ -811,7 +504,7 @@ public synchronized static boolean getFullscreen() { return fullscreen; } - public synchronized static void setFullscreen(boolean newFullscreen) { + public synchronized static void setFullscreen(boolean newFullscreen) { fullscreen = newFullscreen; } @@ -847,28 +540,9 @@ public synchronized static void setTargetMode(int tm) { targetMode = tm; } - private void setTargetModeOrientation() { - if ((targetMode == ROTATE_TO_SHOOT) && - AccelerometerManager.isSupported(getApplicationContext())) { - AccelerometerManager.startListening(getApplicationContext(),this); - // - // In API level 9, SCREEN_ORIENTATION_SENSOR_PORTRAIT was added - // to ActivityInfo. This mode was actually supported by earlier - // APIs, but a definition was not yet explicity defined. - // - // This mode allows the device to display the screen in either - // normal or reverse portrait mode based on the device - // orientation reported by the accelerometer hardware. - // - // - setRequestedOrientation(SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } - - if ((targetMode != ROTATE_TO_SHOOT) && AccelerometerManager.isListening()) { - AccelerometerManager.stopListening(); - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); - } - } + /* + * Following are general utility functions. + */ public void cleanUp() { if (AccelerometerManager.isListening()) @@ -898,6 +572,96 @@ private void cleanUpGameView() { mMultiplayerGameThread = null; } + private int getScreenOrientation() { + /* + * The method getOrientation() was deprecated in API level 8. + * + * For API level 8 or greater, use getRotation(). + */ + int rotation = getWindowManager().getDefaultDisplay().getOrientation(); + DisplayMetrics dm = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(dm); + int width = dm.widthPixels; + int height = dm.heightPixels; + int orientation; + /* + * The orientation determination is based on the natural orienation + * mode of the device, which can be either portrait, landscape, or + * square. + * + * After the natural orientation is determined, convert the device + * rotation into a fully qualified orientation. + */ + if ((((rotation == Surface.ROTATION_0 ) || + (rotation == Surface.ROTATION_180)) && (height > width)) || + (((rotation == Surface.ROTATION_90 ) || + (rotation == Surface.ROTATION_270)) && (width > height))) { + /* + * Natural orientation is portrait. + */ + switch(rotation) { + case Surface.ROTATION_0: + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + break; + case Surface.ROTATION_90: + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + break; + case Surface.ROTATION_180: + orientation = SCREEN_ORIENTATION_REVERSE_PORTRAIT; + break; + case Surface.ROTATION_270: + orientation = SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + break; + default: + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + break; + } + } + else { + /* + * Natural orientation is landscape or square. + */ + switch(rotation) { + case Surface.ROTATION_0: + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + break; + case Surface.ROTATION_90: + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + break; + case Surface.ROTATION_180: + orientation = SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + break; + case Surface.ROTATION_270: + orientation = SCREEN_ORIENTATION_REVERSE_PORTRAIT; + break; + default: + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + break; + } + } + + return orientation; + } + + /** + * Restore the game options from the saved preferences, and register + * the device orientation listener to detect orientation changes. + */ + private void initGameOptions() { + restoreGamePrefs(); + + currentOrientation = getScreenOrientation(); + myOrientationEventListener = + new OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int arg0) { + currentOrientation = getScreenOrientation(); + } + }; + if (myOrientationEventListener.canDetectOrientation()) + myOrientationEventListener.enable(); + } + /** * Start a new game and music player. */ @@ -913,71 +677,98 @@ public void newGame() { playMusic(false); } + private void newGameDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(FrozenBubble.this); + /* + * Set the dialog title. + */ + builder.setTitle(R.string.menu_new_game) + /* + * Set the action buttons. + */ + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + // User clicked OK. Start a new game. + newGame(); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + // User clicked Cancel. Do nothing. + } + }); + builder.create(); + builder.show(); + } + public void onAccelerationChanged(float x, float y, float z) { if (mGameThread != null) { if (currentOrientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT) x = -x; - mGameThread.setPosition(20f+x*2f); + mGameThread.setPosition(20.0f+x*2.0f); } if (mMultiplayerGameThread != null) { - if (currentOrientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT) + if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + x = -y; + else if (currentOrientation == SCREEN_ORIENTATION_REVERSE_LANDSCAPE) + x = y; + else if (currentOrientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT) x = -x; - mMultiplayerGameThread.setPosition(20f+x*2f); + mMultiplayerGameThread.setPosition(20.0f+x*2.0f); } } - public void onGameEvent(int event) { + public void onGameEvent(eventEnum event) { switch (event) { - case GameView.EVENT_GAME_WON: + case GAME_WON: break; - case GameView.EVENT_GAME_LOST: + case GAME_LOST: break; - case GameView.EVENT_GAME_PAUSED: + case GAME_PAUSED: saveState(); if (myModPlayer != null) myModPlayer.pausePlay(); break; - case GameView.EVENT_GAME_RESUME: + case GAME_RESUME: if (myModPlayer == null) playMusic(true); else if (allowUnpause) myModPlayer.unPausePlay(); break; - case GameView.EVENT_LEVEL_START: + case LEVEL_START: if ((mGameView != null) && (mGameView.getThread().getCurrentLevelIndex() == 0)) { - // - // Destroy the current music player, which will free audio - // stream resources and allow the system to use them. - // - // + /* + * Destroy the current music player, which will free audio + * stream resources and allow the system to use them. + */ if (myModPlayer != null) myModPlayer.destroyMusicPlayer(); myModPlayer = null; - // - // Clear the game screen and suspend input processing for - // three seconds. - // - // Afterwards, the "About" screen will be displayed as a - // backup just in case anything goes awry with displaying - // the end-of-game credits. It will be displayed after the - // user touches the screen when the credits are finished. - // - // + /* + * Clear the game screen and suspend input processing for + * three seconds. + * + * Afterwards, the "About" screen will be displayed as a + * backup just in case anything goes awry with displaying + * the end-of-game credits. It will be displayed after the + * user touches the screen when the credits are finished. + */ mGameView.clearGameScreen(true, 3000); - // - // Create an intent to launch the activity to display the - // credits screen. - // - // + /* + * Create an intent to launch the activity to display the + * credits screen. + */ Intent intent = new Intent(this, ScrollingCredits.class); startActivity(intent); } @@ -1000,7 +791,9 @@ private void pause() { if (mMultiplayerGameView != null) mMultiplayerGameView.getThread().pause(); - // Pause the MOD player. + /* + * Pause the MOD player. + */ if (myModPlayer != null) myModPlayer.pausePlay(); } @@ -1010,15 +803,16 @@ private void pause() { * be created or if one already exists. Then, based on the current * level, the song to play is calculated and loaded. If desired, the * song will start playing immediately, or it can remain paused. - * - * @param startPlaying - * - If true, the song starts playing immediately. Otherwise - * it is paused and must be unpaused to start playing. + * @param startPlaying - If true, the song starts playing + * immediately. Otherwise it is paused and must be unpaused to start + * playing. */ private void playMusic(boolean startPlaying) { int modNow; - // Ascertain which song to play. + /* + * Ascertain which song to play. + */ if (mGameView != null) modNow = mGameView.getThread().getCurrentLevelIndex() % MODlist.length; else @@ -1026,7 +820,9 @@ private void playMusic(boolean startPlaying) Random rand = new Random(); modNow = rand.nextInt(MODlist.length - 1); } - // Determine whether to create a music player or load the song. + /* + * Determine whether to create a music player or load the song. + */ if (myModPlayer == null) myModPlayer = new ModPlayer(this, MODlist[modNow], getMusicOn(), !startPlaying); @@ -1035,48 +831,283 @@ private void playMusic(boolean startPlaying) allowUnpause = true; } + private void restoreGamePrefs() { + SharedPreferences mConfig = getSharedPreferences(PREFS_NAME, + Context.MODE_PRIVATE); + adsOn = mConfig.getBoolean("adsOn", true ); + collision = mConfig.getInt ("collision", BubbleSprite.MIN_PIX ); + compressor = mConfig.getBoolean("compressor", false ); + difficulty = mConfig.getInt ("difficulty", LevelManager.MODERATE); + dontRushMe = mConfig.getBoolean("dontRushMe", false ); + fullscreen = mConfig.getBoolean("fullscreen", true ); + gameMode = mConfig.getInt ("gameMode", GAME_NORMAL ); + musicOn = mConfig.getBoolean("musicOn", true ); + soundOn = mConfig.getBoolean("soundOn", true ); + targetMode = mConfig.getInt ("targetMode", POINT_TO_SHOOT ); + + BubbleSprite.setCollisionThreshold(collision); + setTargetMode(targetMode); + setTargetModeOrientation(); + } + /** * Save critically important game information. */ public void saveState() { if (mGameView != null) { - // Allow editor functionalities. + /* + * Allow level editor functionalities. + */ SharedPreferences sp = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); - // If I didn't run game from editor, save last played level. + /* + * If the game wasn't launched from the level editor, save the + * last level played. + */ Intent i = getIntent(); if ((null == i) || !activityCustomStarted) { editor.putInt("level", mGameThread.getCurrentLevelIndex()); } else { - // Editor's intent is running. + /* + * The level editor's intent is running. + */ editor.putInt("levelCustom", mGameThread.getCurrentLevelIndex()); } editor.commit(); } } + private void setFullscreen() { + final int flagFs = WindowManager.LayoutParams.FLAG_FULLSCREEN; + final int flagNoFs = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; + + if (fullscreen) { + getWindow().addFlags(flagFs); + getWindow().clearFlags(flagNoFs); + } + else { + getWindow().clearFlags(flagFs); + getWindow().addFlags(flagNoFs); + } + + if (mGameView != null) + mGameView.requestLayout(); + + if (mMultiplayerGameView != null) + mMultiplayerGameView.requestLayout(); + } + + private void setTargetModeOrientation() { + if ((targetMode == ROTATE_TO_SHOOT) && + AccelerometerManager.isSupported(getApplicationContext())) { + AccelerometerManager.startListening(getApplicationContext(),this); + /* + * In API level 9, SCREEN_ORIENTATION_SENSOR_PORTRAIT and + * SCREEN_ORIENTATION_SENSOR_LANDSCAPE were added to ActivityInfo. + * This application is developed in API level 4, but using these + * values will be supported correctly in devices with a native API + * level that implements this functionality. + * + * These modes allow the device to display the screen in either + * normal or reverse portrait mode based on the device orientation + * reported by the accelerometer hardware. + * + * For multiplayer games using rotate to shoot, set the + * orientation to sensor landscape, and for single player games, + * set the orientation to sensor portrait. + */ + if (numPlayers > 1) { + setRequestedOrientation(SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } + else { + setRequestedOrientation(SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + } + + if ((targetMode != ROTATE_TO_SHOOT) && + AccelerometerManager.isListening()) { + AccelerometerManager.stopListening(); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); + } + } + + private void soundOptionsDialog() { + boolean isCheckedItem[] = {getSoundOn(), getMusicOn()}; + + AlertDialog.Builder builder = new AlertDialog.Builder(FrozenBubble.this); + /* + * Set the dialog title. + */ + builder.setTitle(R.string.menu_sound_options) + /* + * Specify the list array, the items to be selected by default (null + * for none), and the listener through which to receive callbacks + * when items are selected. + */ + .setMultiChoiceItems(R.array.sound_options_array, isCheckedItem, + new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + SharedPreferences sp = getSharedPreferences(PREFS_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + + switch (which) { + case 0: + setSoundOn(isChecked); + editor.putBoolean("soundOn", soundOn); + editor.commit(); + break; + case 1: + setMusicOn(isChecked); + if (myModPlayer != null) { + myModPlayer.setMusicOn(isChecked); + } + editor.putBoolean("musicOn", musicOn); + editor.commit(); + break; + } + } + }) + /* + * Set the action buttons. + */ + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + // User clicked OK. + } + }); + builder.create(); + builder.show(); + } + + /** + * Method to start a game using levels from the level editor. + *

If the level isn't specified from the editor, then the player + * selected the option to continue playing from the last level + * played, so use the last level played instead. + * @param intent - The intent from the level editor used to start this + * activity, which contains the custom level data. + */ + private void startCustomGame(Intent intent) { + activityCustomStarted = true; + numPlayers = 1; + initGameOptions(); + /* + * Get custom level last played. + */ + SharedPreferences sp = getSharedPreferences(PREFS_NAME, + Context.MODE_PRIVATE); + int startingLevel = sp .getInt ("levelCustom", 0); + int startingLevelIntent = intent.getIntExtra("startingLevel", -2); + startingLevel = + (startingLevelIntent == -2) ? startingLevel : startingLevelIntent; + mGameView = new GameView(this, + intent.getExtras().getByteArray("levels"), + startingLevel); + setContentView(mGameView); + mGameView.setGameListener(this); + mGameThread = mGameView.getThread(); + mGameView.requestFocus(); + setFullscreen(); + playMusic(false); + } + + /** + * Method to start a game using default levels, if single player game + * mode was selected. + *

This method is also used to start a multiplayer game. + * @param intent - The intent used to start this activity. + * @param savedInstanceState - the bundle of saved state information. + */ + private void startDefaultGame(Intent intent, Bundle savedInstanceState) { + /* + * Initialize the flag to denote this game uses default levels. + */ + activityCustomStarted = false; + /* + * Check if this is a single player or multiplayer game. + */ + numPlayers = 1; + gameLocale = LOCALE_LOCAL; + if (intent != null) { + if (intent.hasExtra("myPlayerId")) + myPlayerId = intent.getIntExtra("myPlayerId", VirtualInput.PLAYER1); + if (intent.hasExtra("numPlayers")) + numPlayers = intent.getIntExtra("numPlayers", 1); + if (intent.hasExtra("gameLocale")) + gameLocale = intent.getIntExtra("gameLocale", LOCALE_LOCAL); + } + initGameOptions(); + /* + * If there is more than one player, launch a multiplayer game. + * Otherwise start a single player game. + */ + if (numPlayers > 1) { + mMultiplayerGameView = new MultiplayerGameView(this, + myPlayerId, + gameLocale); + setContentView(mMultiplayerGameView); + mMultiplayerGameView.setGameListener(this); + mMultiplayerGameThread = mMultiplayerGameView.getThread(); + /* + * Only restore the bundle for a multiplayer game if it was local. + */ + if ((savedInstanceState != null) && (gameLocale == LOCALE_LOCAL)) { + int savedPlayers = savedInstanceState.getInt("numPlayers"); + if (savedPlayers == 2) { + mMultiplayerGameThread.restoreState(savedInstanceState); + } + } + mMultiplayerGameThread.startOpponent(); + mMultiplayerGameView.requestFocus(); + } + else { + setContentView(R.layout.activity_frozen_bubble); + mGameView = (GameView)findViewById(R.id.game); + mGameView.setGameListener(this); + mGameThread = mGameView.getThread(); + if (savedInstanceState != null) { + int savedPlayers = savedInstanceState.getInt("numPlayers"); + if (savedPlayers == 1) + mGameThread.restoreState(savedInstanceState); + } + mGameView.requestFocus(); + } + setFullscreen(); + playMusic(false); + } + /** * Starts editor / market with editor's download. */ private void startEditor() { Intent i = new Intent(); - // First try to run the plus version of Editor. + /* + * First try to run the plus version of the level editor. + */ i.setClassName("sk.halmi.fbeditplus", "sk.halmi.fbeditplus.EditorActivity"); try { startActivity(i); finish(); } catch (ActivityNotFoundException e) { - // If not found, try to run the normal version. + /* + * If not found, try to run the normal version. + */ i.setClassName("sk.halmi.fbedit", "sk.halmi.fbedit.EditorActivity"); try { startActivity(i); finish(); } catch (ActivityNotFoundException ex) { - // If user doesnt have Frozen Bubble Editor take him to market. + /* + * If the user doesn't have the Frozen Bubble Level Editor, take + * him to the application market. + */ try { Toast.makeText(getApplicationContext(), R.string.install_editor, Toast.LENGTH_SHORT).show(); @@ -1085,11 +1116,61 @@ private void startEditor() { "market://search?q=frozen bubble level editor")); startActivity(i); } catch (Exception exc) { - // Damn you don't have market? + /* + * Damn, you don't have market? + */ Toast.makeText(getApplicationContext(), R.string.market_missing, Toast.LENGTH_SHORT).show(); } } } } + + private void targetOptionsDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(FrozenBubble.this); + /* + * Set the dialog title. + */ + builder.setTitle(R.string.menu_target_mode) + /* + * Specify the list array, the item to be selected by default, and + * the listener through which to receive callbacks when the item is + * selected. + */ + .setSingleChoiceItems(R.array.shoot_mode_array, targetMode, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface builder, int which) { + switch (which) { + case 0: + setTargetMode(AIM_TO_SHOOT); + break; + case 1: + setTargetMode(POINT_TO_SHOOT); + break; + case 2: + setTargetMode(ROTATE_TO_SHOOT); + break; + } + setTargetModeOrientation(); + } + }) + /* + * Set the action buttons. + */ + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface builder, int id) { + // User clicked OK. + SharedPreferences sp = getSharedPreferences(PREFS_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putInt("targetMode", targetMode); + editor.commit(); + } + }); + + builder.create(); + builder.show(); + } } diff --git a/src/org/jfedor/frozenbubble/FrozenGame.java b/src/org/jfedor/frozenbubble/FrozenGame.java index 82998fb..4c50b83 100644 --- a/src/org/jfedor/frozenbubble/FrozenGame.java +++ b/src/org/jfedor/frozenbubble/FrozenGame.java @@ -64,9 +64,16 @@ import android.os.Bundle; import android.util.Log; +import com.efortin.frozenbubble.CRC16; import com.efortin.frozenbubble.HighscoreManager; +import com.efortin.frozenbubble.NetworkGameManager; +import com.efortin.frozenbubble.VirtualInput; public class FrozenGame extends GameScreen { + private final int[] columnX = { 190, 206, 232, 248, 264, + 280, 296, 312, 328, 344, + 360, 376, 392, 408, 424 }; + public final static int HORIZONTAL_MOVE = 0; public final static int FIRE = 1; @@ -79,12 +86,6 @@ public class FrozenGame extends GameScreen { public final static int KEY_RIGHT = 39; public final static int KEY_SHIFT = 16; - public static final int GAME_PLAYING = 1; - public static final int GAME_LOST = 2; - public static final int GAME_WON = 3; - public static final int GAME_NEXT_LOST = 4; - public static final int GAME_NEXT_WON = 5; - public static final int HURRY_ME_TIME = 480; public static final int RELEASE_TIME = 300; @@ -102,13 +103,14 @@ public class FrozenGame extends GameScreen { Compressor compressor; ImageSprite nextBubble; - int currentColor, nextColor; + int currentColor, nextColor, newNextColor; BubbleSprite movingBubble; BubbleManager bubbleManager; LevelManager levelManager; MalusBar malusBar; HighscoreManager highscoreManager; + NetworkGameManager networkManager; Vector falling; Vector goingUp; @@ -132,15 +134,16 @@ public class FrozenGame extends GameScreen { boolean endOfGame; boolean frozenify; + boolean isRemote; boolean readyToFire; boolean swapPressed; + gameEnum playResult; + short gridChecksum; int fixedBubbles; int frozenifyX, frozenifyY; int nbBubbles; int player; - int playResult; int sendToOpponent; - double moveDown; Drawable launcher; BmpWrap penguins; @@ -165,7 +168,8 @@ public FrozenGame(BmpWrap background_arg, SoundManager soundManager_arg, LevelManager levelManager_arg, HighscoreManager highscoreManager_arg, - int player_arg) { + NetworkGameManager networkManager_arg, + VirtualInput input_arg) { random = new Random(System.currentTimeMillis()); launcher = launcher_arg; penguins = penguins_arg; @@ -180,13 +184,28 @@ public FrozenGame(BmpWrap background_arg, soundManager = soundManager_arg; levelManager = levelManager_arg; highscoreManager = highscoreManager_arg; + networkManager = networkManager_arg; malusBar = malusBar_arg; - player = player_arg; - playResult = GAME_PLAYING; + playResult = gameEnum.PLAYING; launchBubblePosition = START_LAUNCH_DIRECTION; readyToFire = false; swapPressed = false; + /* + * Initialize game modifier variables. + */ + if (input_arg != null) { + player = input_arg.playerID; + isRemote = input_arg.isRemote; + } + else { + player = VirtualInput.PLAYER1; + isRemote = false; + } + + /* + * Create objects for all the game graphics. + */ if ((pauseButton_arg != null) && (playButton_arg != null)) { pauseButtonSprite = new ImageSprite(new Rect(167, 444, 32, 32), pauseButton_arg); @@ -195,8 +214,7 @@ public FrozenGame(BmpWrap background_arg, this.addSprite(pauseButtonSprite); } - penguin = new PenguinSprite(PenguinSprite.getPenguinRect(player), - penguins_arg, random); + penguin = new PenguinSprite(getPenguinRect(player), penguins_arg, random); this.addSprite(penguin); compressor = new Compressor(compressorHead_arg, compressor_arg); @@ -215,14 +233,17 @@ public FrozenGame(BmpWrap background_arg, bubblePlay = new BubbleSprite[8][13]; bubbleManager = new BubbleManager(bubbles); + /* + * Load the current level to the bubble play grid. + */ byte[][] currentLevel = levelManager.getCurrentLevel(); if (currentLevel == null) { //Log.i("frozen-bubble", "Level not available."); return; } - for (int j=0 ; j<12 ; j++) { - for (int i=j%2 ; i<8 ; i++) { + for (int j = 0; j < 12; j++) { + for (int i = j%2; i < 8; i++) { if (currentLevel[i][j] != -1) { BubbleSprite newOne = new BubbleSprite( new Rect(190+i*32-(j%2)*16, 44+j*28, 32, 32), @@ -236,6 +257,9 @@ public FrozenGame(BmpWrap background_arg, } } + /* + * Initialize the launch bubbles. + */ currentColor = bubbleManager.nextBubbleIndex(random); nextColor = bubbleManager.nextBubbleIndex(random); @@ -253,7 +277,12 @@ public FrozenGame(BmpWrap background_arg, launchBubblePosition, launcher, bubbles, bubblesBlind); this.spriteToBack(launchBubble); - nbBubbles = 0; + + /* + * Initialize game metrics. + */ + nbBubbles = 0; + sendToOpponent = 0; } public FrozenGame(BmpWrap background_arg, @@ -277,295 +306,126 @@ public FrozenGame(BmpWrap background_arg, targetedBubbles_arg, bubbleBlink_arg, gameWon_arg, gameLost_arg, gamePaused_arg, hurry_arg, null, null, penguins_arg, compressorHead_arg, compressor_arg, null, launcher_arg, soundManager_arg, - levelManager_arg, highscoreManager_arg, 1); + levelManager_arg, highscoreManager_arg, null, null); } - public void saveState(Bundle map) { - Vector savedSprites = new Vector(); - saveSprites(map, savedSprites, player); - for (int i = 0; i < jumping.size(); i++) { - ((Sprite)jumping.elementAt(i)).saveState(map, savedSprites, player); - map.putInt(String.format("%d-jumping-%d", player, i), - ((Sprite)jumping.elementAt(i)).getSavedId()); - } - map.putInt(String.format("%d-numJumpingSprites", player), jumping.size()); - for (int i = 0; i < goingUp.size(); i++) { - ((Sprite)goingUp.elementAt(i)).saveState(map, savedSprites, player); - map.putInt(String.format("%d-goingUp-%d", player, i), - ((Sprite)goingUp.elementAt(i)).getSavedId()); - } - map.putInt(String.format("%d-numGoingUpSprites", player), goingUp.size()); - for (int i = 0; i < falling.size(); i++) { - ((Sprite)falling.elementAt(i)).saveState(map, savedSprites, player); - map.putInt(String.format("%d-falling-%d", player, i), - ((Sprite)falling.elementAt(i)).getSavedId()); + public void addAttackBubbles(int attackBubbles) { + sendToOpponent += attackBubbles; + } + + public void addFallingBubble(BubbleSprite sprite) { + if (malusBar != null) + malusBar.releaseTime = 0; + sendToOpponent++; + spriteToFront(sprite); + falling.addElement(sprite); + } + + public void addJumpingBubble(BubbleSprite sprite) { + spriteToFront(sprite); + jumping.addElement(sprite); + } + + private void blinkLine(int number) { + int move = number%2; + int column = (number+1) >> 1; + + for (int i = move; i < 13; i++) { + if (bubblePlay[column][i] != null) { + bubblePlay[column][i].blink(); + } } - map.putInt(String.format("%d-numFallingSprites", player), falling.size()); + } + + public void calculateGridChecksum() { + CRC16 gridCRC = new CRC16(0); + for (int i = 0; i < 8; i++) { - for (int j = 0; j < 13; j++) { + for (int j = 0; j < 12; j++) { if (bubblePlay[i][j] != null) { - bubblePlay[i][j].saveState(map, savedSprites, player); - map.putInt(String.format("%d-play-%d-%d", player, i, j), - bubblePlay[i][j].getSavedId()); - } - else { - map.putInt(String.format("%d-play-%d-%d", player, i, j), -1); + gridCRC.update(bubblePlay[i][j].getColor()); } } } - launchBubble.saveState(map, savedSprites, player); - map.putInt(String.format("%d-launchBubbleId", player), - launchBubble.getSavedId()); - map.putDouble(String.format("%d-launchBubblePosition", player), - launchBubblePosition); - if (malusBar != null) { - malusBar.saveState(map, player); - } - penguin.saveState(map, savedSprites, player); - compressor.saveState(map, player); - map.putInt(String.format("%d-penguinId", player), penguin.getSavedId()); - nextBubble.saveState(map, savedSprites, player); - map.putInt(String.format("%d-nextBubbleId", player), - nextBubble.getSavedId()); - map.putInt(String.format("%d-currentColor", player), currentColor); - map.putInt(String.format("%d-nextColor", player), nextColor); - if (movingBubble != null) { - movingBubble.saveState(map, savedSprites, player); - map.putInt(String.format("%d-movingBubbleId", player), - movingBubble.getSavedId()); - } - else { - map.putInt(String.format("%d-movingBubbleId", player), -1); - } - bubbleManager.saveState(map, player); - map.putInt(String.format("%d-fixedBubbles", player), fixedBubbles); - map.putDouble(String.format("%d-moveDown", player), moveDown); - map.putInt(String.format("%d-nbBubbles", player), nbBubbles); - map.putInt(String.format("%d-playResult", player), playResult); - map.putInt(String.format("%d-sendToOpponent", player), sendToOpponent); - map.putInt(String.format("%d-blinkDelay", player), blinkDelay); - hurrySprite.saveState(map, savedSprites, player); - map.putInt(String.format("%d-hurryId", player), hurrySprite.getSavedId()); - map.putInt(String.format("%d-hurryTime", player), hurryTime); - pausedSprite.saveState(map, savedSprites, player); - map.putInt(String.format("%d-pausedId", player), pausedSprite.getSavedId()); - map.putBoolean(String.format("%d-readyToFire", player), readyToFire); - map.putBoolean(String.format("%d-endOfGame", player), endOfGame); - map.putBoolean(String.format("%d-frozenify", player), frozenify); - map.putInt(String.format("%d-frozenifyX", player), frozenifyX); - map.putInt(String.format("%d-frozenifyY", player), frozenifyY); - map.putInt(String.format("%d-numSavedSprites", player), - savedSprites.size()); - for (int i = 0; i < savedSprites.size(); i++) { - ((Sprite)savedSprites.elementAt(i)).clearSavedId(); - } + gridChecksum = (short) gridCRC.getValue(); } - private Sprite restoreSprite(Bundle map, Vector imageList, int i) { - int left = map.getInt(String.format("%d-%d-left", player, i)); - int right = map.getInt(String.format("%d-%d-right", player, i)); - int top = map.getInt(String.format("%d-%d-top", player, i)); - int bottom = map.getInt(String.format("%d-%d-bottom", player, i)); - int type = map.getInt(String.format("%d-%d-type", player, i)); - if (type == Sprite.TYPE_BUBBLE) { - int color = map.getInt(String.format("%d-%d-color", player, i)); - double moveX = map.getDouble(String.format("%d-%d-moveX", player, i)); - double moveY = map.getDouble(String.format("%d-%d-moveY", player, i)); - double realX = map.getDouble(String.format("%d-%d-realX", player, i)); - double realY = map.getDouble(String.format("%d-%d-realY", player, i)); - boolean fixed = map.getBoolean(String.format("%d-%d-fixed", player, i)); - boolean blink = map.getBoolean(String.format("%d-%d-blink", player, i)); - boolean released = - map.getBoolean(String.format("%d-%d-released", player, i)); - boolean checkJump = - map.getBoolean(String.format("%d-%d-checkJump", player, i)); - boolean checkFall = - map.getBoolean(String.format("%d-%d-checkFall", player, i)); - int fixedAnim = map.getInt(String.format("%d-%d-fixedAnim", player, i)); - boolean frozen = - map.getBoolean(String.format("%d-%d-frozen", player, i)); - Point lastOpenPosition = new Point( - map.getInt(String.format("%d-%d-lastOpenPosition.x", player, i)), - map.getInt(String.format("%d-%d-lastOpenPosition.y", player, i))); - return new BubbleSprite(new Rect(left, top, right, bottom), - color, moveX, moveY, realX, realY, - fixed, blink, released, checkJump, checkFall, - fixedAnim, - (frozen ? frozenBubbles[color] : bubbles[color]), - lastOpenPosition, - bubblesBlind[color], - frozenBubbles[color], - targetedBubbles, bubbleBlink, - bubbleManager, soundManager, this); - } - else if (type == Sprite.TYPE_IMAGE) { - int imageId = map.getInt(String.format("%d-%d-imageId", player, i)); - return new ImageSprite(new Rect(left, top, right, bottom), - (BmpWrap)imageList.elementAt(imageId)); - } - else if (type == Sprite.TYPE_LAUNCH_BUBBLE) { - int currentColor = - map.getInt(String.format("%d-%d-currentColor", player, i)); - double currentDirection = - map.getDouble(String.format("%d-%d-currentDirection", player, i)); - return new LaunchBubbleSprite(currentColor, currentDirection, - launcher, bubbles, bubblesBlind); - } - else if (type == Sprite.TYPE_PENGUIN) { - int currentPenguin = - map.getInt(String.format("%d-%d-currentPenguin", player, i)); - int count = map.getInt(String.format("%d-%d-count", player, i)); - int finalState = - map.getInt(String.format("%d-%d-finalState", player, i)); - int nextPosition = - map.getInt(String.format("%d-%d-nextPosition", player, i)); + private boolean checkLost() { + boolean lost = false; - return new PenguinSprite(PenguinSprite.getPenguinRect(player), - penguins, random, currentPenguin, count, - finalState, nextPosition); - } - else { - Log.e("frozen-bubble", "Unrecognized sprite type: " + type); - return null; + if (!endOfGame) { + if (movingBubble != null) { + if (movingBubble.fixed() && !movingBubble.released() && + (movingBubble.getSpritePosition().y >= 380)) { + lost = true; + } + } + + for (int i = 0; i < 8; i++) { + if (bubblePlay[i][12 - compressor.steps] != null) { + lost = true; + break; + } + } + + if (lost) { + penguin.updateState(PenguinSprite.STATE_GAME_LOST); + if (highscoreManager != null) + highscoreManager.lostLevel(); + playResult = gameEnum.LOST; + endOfGame = true; + initFrozenify(); + soundManager.playSound(FrozenBubble.SOUND_LOST); + } } - } - - public void pause() { - this.removeSprite(pausedSprite); - this.addSprite(pausedSprite); + + return playResult == gameEnum.LOST; } - public void pauseButtonPressed(boolean paused) { - if (paused) { - this.removeSprite(pauseButtonSprite); - this.removeSprite(playButtonSprite); - this.addSprite(playButtonSprite); + public void clampLaunchPosition() { + if (launchBubblePosition < MIN_LAUNCH_DIRECTION) { + launchBubblePosition = MIN_LAUNCH_DIRECTION; } - else { - this.removeSprite(pauseButtonSprite); - this.removeSprite(playButtonSprite); - this.addSprite(pauseButtonSprite); + if (launchBubblePosition > MAX_LAUNCH_DIRECTION) { + launchBubblePosition = MAX_LAUNCH_DIRECTION; } - } - - public void resume() { - this.removeSprite(pausedSprite); - } - - public void restoreState(Bundle map, Vector imageList) { - Vector savedSprites = new Vector(); - int numSavedSprites = - map.getInt(String.format("%d-numSavedSprites", player)); - for (int i = 0; i < numSavedSprites; i++) { - savedSprites.addElement(restoreSprite(map, imageList, i)); - } - - restoreSprites(map, savedSprites, player); - jumping = new Vector(); - int numJumpingSprites = - map.getInt(String.format("%d-numJumpingSprites", player)); - for (int i = 0; i < numJumpingSprites; i++) { - int spriteIdx = map.getInt(String.format("%d-jumping-%d", player, i)); - jumping.addElement(savedSprites.elementAt(spriteIdx)); - } - goingUp = new Vector(); - int numGoingUpSprites = - map.getInt(String.format("%d-numGoingUpSprites", player)); - for (int i = 0; i < numGoingUpSprites; i++) { - int spriteIdx = map.getInt(String.format("%d-goingUp-%d", player, i)); - goingUp.addElement(savedSprites.elementAt(spriteIdx)); - } - falling = new Vector(); - int numFallingSprites = - map.getInt(String.format("%d-numFallingSprites", player)); - for (int i = 0; i < numFallingSprites; i++) { - int spriteIdx = map.getInt(String.format("%d-falling-%d", player, i)); - falling.addElement(savedSprites.elementAt(spriteIdx)); - } - bubblePlay = new BubbleSprite[8][13]; - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 13; j++) { - int spriteIdx = - map.getInt(String.format("%d-play-%d-%d", player, i, j)); - if (spriteIdx != -1) { - bubblePlay[i][j] = (BubbleSprite)savedSprites.elementAt(spriteIdx); - } - else { - bubblePlay[i][j] = null; - } - } - } - int launchBubbleId = - map.getInt(String.format("%d-launchBubbleId", player)); - launchBubble = (LaunchBubbleSprite)savedSprites.elementAt(launchBubbleId); - launchBubblePosition = - map.getDouble(String.format("%d-launchBubblePosition", player)); - if (malusBar != null) { - malusBar.restoreState(map, player); - this.addSprite(malusBar); - } - int penguinId = map.getInt(String.format("%d-penguinId", player)); - penguin = (PenguinSprite)savedSprites.elementAt(penguinId); - compressor.restoreState(map, player); - int nextBubbleId = map.getInt(String.format("%d-nextBubbleId", player)); - nextBubble = (ImageSprite)savedSprites.elementAt(nextBubbleId); - currentColor = map.getInt(String.format("%d-currentColor", player)); - nextColor = map.getInt(String.format("%d-nextColor", player)); - int movingBubbleId = - map.getInt(String.format("%d-movingBubbleId", player)); - if (movingBubbleId == -1) { - movingBubble = null; - } - else { - movingBubble = (BubbleSprite)savedSprites.elementAt(movingBubbleId); - } - bubbleManager.restoreState(map, player); - fixedBubbles = map.getInt(String.format("%d-fixedBubbles", player)); - moveDown = map.getDouble(String.format("%d-moveDown", player)); - nbBubbles = map.getInt(String.format("%d-nbBubbles", player)); - playResult = map.getInt(String.format("%d-playResult", player)); - sendToOpponent = map.getInt(String.format("%d-sendToOpponent", player)); - blinkDelay = map.getInt(String.format("%d-blinkDelay", player)); - int hurryId = map.getInt(String.format("%d-hurryId", player)); - hurrySprite = (ImageSprite)savedSprites.elementAt(hurryId); - hurryTime = map.getInt(String.format("%d-hurryTime", player)); - int pausedId = map.getInt(String.format("%d-pausedId", player)); - pausedSprite = (ImageSprite)savedSprites.elementAt(pausedId); - readyToFire = map.getBoolean(String.format("%d-readyToFire", player)); - endOfGame = map.getBoolean(String.format("%d-endOfGame", player)); - frozenify = map.getBoolean(String.format("%d-frozenify", player)); - frozenifyX = map.getInt(String.format("%d-frozenifyX", player)); - frozenifyY = map.getInt(String.format("%d-frozenifyY", player)); - } - - private void initFrozenify() { - ImageSprite freezeLaunchBubble = - new ImageSprite(new Rect(301, 389, 34, 42), frozenBubbles[currentColor]); - ImageSprite freezeNextBubble = - new ImageSprite(new Rect(301, 439, 34, 42), frozenBubbles[nextColor]); - - this.addSprite(freezeLaunchBubble); - this.addSprite(freezeNextBubble); - - frozenifyX = 7; - frozenifyY = 12; - frozenify = true; - } - - private void frozenify() { - frozenifyX--; - if (frozenifyX < 0) { - frozenifyX = 7; - frozenifyY--; - - if (frozenifyY<0) { - frozenify = false; - this.addSprite(new ImageSprite(new Rect(152, 190, 337, 116), - gameLost)); - soundManager.playSound(FrozenBubble.SOUND_NOH); - return; - } + } + + public void deleteFallingBubble(BubbleSprite sprite) { + removeSprite(sprite); + falling.removeElement(sprite); + } + + /** + * Remove the designated goingUp bubble sprite from the vector of + * attack bubbles because it is now inserted into the game grid. The + * sprite is not removed from the vector of all sprites in the game + * because it has been added to the play field. + * @param sprite - the attack bubble inserted into the game grid. + */ + public void deleteGoingUpBubble(BubbleSprite sprite) { + goingUp.removeElement(sprite); + } + + public void deleteJumpingBubble(BubbleSprite sprite) { + removeSprite(sprite); + jumping.removeElement(sprite); + } + + private void frozenify() { + frozenifyX--; + if (frozenifyX < 0) { + frozenifyX = 7; + frozenifyY--; + + if (frozenifyY < 0) { + frozenify = false; + this.addSprite(new ImageSprite(new Rect(152, 190, 337, 116), + gameLost)); + soundManager.playSound(FrozenBubble.SOUND_NOH); + return; + } } while (bubblePlay[frozenifyX][frozenifyY] == null && frozenifyY >=0) { @@ -574,7 +434,7 @@ private void frozenify() { frozenifyX = 7; frozenifyY--; - if (frozenifyY<0) { + if (frozenifyY < 0) { frozenify = false; this.addSprite(new ImageSprite(new Rect(152, 190, 337, 116), gameLost)); @@ -590,246 +450,208 @@ private void frozenify() { this.spriteToBack(launchBubble); } - public BubbleSprite[][] getGrid() { - return bubblePlay; + public int getAttackBarBubbles() { + return malusBar.getAttackBarBubbles(); } - public void addFallingBubble(BubbleSprite sprite) { - if (malusBar != null) - malusBar.releaseTime = 0; - sendToOpponent++; - spriteToFront(sprite); - falling.addElement(sprite); + public int getCompressorSteps() { + return compressor.steps; } - public void addJumpingBubble(BubbleSprite sprite) { - spriteToFront(sprite); - jumping.addElement(sprite); + public int getCurrentColor() { + return currentColor; } - private void blinkLine(int number) { - int move = number % 2; - int column = (number+1) >> 1; - - for (int i=move ; i<13 ; i++) { - if (bubblePlay[column][i] != null) { - bubblePlay[column][i].blink(); - } - } + public gameEnum getGameResult() { + return playResult; } - private boolean checkLost() { - boolean lost = false; - - if (!endOfGame) { - if (movingBubble != null) { - if (movingBubble.fixed()) { - if (movingBubble.getSpritePosition().y>=380 && - !movingBubble.released()) { - lost = true; - } - } - } - - for (int i = 0; i < 8; i++) { - if (bubblePlay[i][12 - compressor.steps] != null) { - lost = true; - break; - } - } - - if (lost) { - penguin.updateState(PenguinSprite.STATE_GAME_LOST); - if (highscoreManager != null) - highscoreManager.lostLevel(); - playResult = GAME_LOST; - endOfGame = true; - initFrozenify(); - soundManager.playSound(FrozenBubble.SOUND_LOST); - } - } + public BubbleSprite[][] getGrid() { + return bubblePlay; + } - return (playResult == GAME_LOST); + public double getMoveDown() { + return compressor.getMoveDown(); } - /** - * This function is an unfortunate patch that is necessitated due to - * the fact that there is as of yet an unfixed bug in the BubbleSprite - * management code. - *

- * Somewhere amongst goUp() and move() in BubbleSprite.java, a flaw - * exists whereby a bubble is added to the bubble manager, and the - * bubble sprite is added to the game screen, but the entry in the - * bubblePlay grid was either rendered null or a bubble superposition - * in the grid occurred. The former is suspected, because ensuring - * the grid location is null before assigning a bubble sprite to it is - * very rigorously enforced. - *

- * TODO - fix the grid entry nullification/superposition bug. - */ - public void synchronizeBubbleManager() { - int numBubblesManager = bubbleManager.countBubbles(); - int numBubblesPlay = 0; - /* - * Check the bubble sprite grid for occupied locations. - */ - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 13; j++) { - if (bubblePlay[i][j] != null ) { - numBubblesPlay++; - } - } - } - /* - * If the number of bubble sprite grid entries does not match the - * number of bubbles in the bubble manager, then we need to re- - * initialize the bubble manager, and re-initialize all the bubble - * sprites on the game screen. You would be unable to win prior to - * the addition of this synchronization code due to the number of - * bubbles in the bubble manager never reaching zero, and the excess - * sprite or sprites would remain stuck on the screen. - */ - if (numBubblesManager != numBubblesPlay) { - bubbleManager.initialize(); - removeAllBubbleSprites(); - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 13; j++) { - if (bubblePlay[i][j] != null ) { - bubblePlay[i][j].addToManager(); - this.addSprite(bubblePlay[i][j]); - } - } - } - for (int i=0 ; isendToOpponent value, which is + * the number of attack bubbles to add to the opponent's attack bar. + * @return The number of attack bubbles to add to the opponent's + * attack bar. + */ public int getSendToOpponent() { - return sendToOpponent; + return sendToOpponent; + } + + private void initFrozenify() { + ImageSprite freezeLaunchBubble = + new ImageSprite(new Rect(301, 389, 34, 42), frozenBubbles[currentColor]); + ImageSprite freezeNextBubble = + new ImageSprite(new Rect(301, 439, 34, 42), frozenBubbles[nextColor]); + + this.addSprite(freezeLaunchBubble); + this.addSprite(freezeNextBubble); + + frozenifyX = 7; + frozenifyY = 12; + frozenify = true; } /** - * Populate random columns in a row of attack bubbles to launch onto - * the game field. - *

- * In an actual play field, the rows alternate between a maximum 7 and - * 8 bubbles per row. Thus 7 bubbles are sent up as that is the - * maximum number of bubbles that can fit in each alternating row. - *

- * There are 15 distinct positions ("lanes") for bubbles to occupy - * between two consecutive rows. Thus we send up a maximum 7 bubbles - * in randomly selected "lanes" from the 15 available. + * Lower the bubbles in play and drop the compressor a step. + * @param playSound - true to play the compression sound. */ - private void releaseBubbles() { - if ((malusBar != null) && (malusBar.getBubbles() > 0)) { - final int[] columnX = { 190, 206, 232, 248, 264, - 280, 296, 312, 328, 344, - 360, 376, 392, 408, 424 }; - boolean[] lanes = new boolean[15]; - int malusBalls = malusBar.removeLine(); - int pos; + public void lowerCompressor(boolean playSound) { + fixedBubbles = 0; - while (malusBalls > 0) { - pos = random.nextInt(15); - if (!lanes[pos]) { - lanes[pos] = true; - malusBalls--; - } - } - - for (int i = 0; i < 15; i++) { - if (lanes[i]) { - int color = random.nextInt(FrozenBubble.getDifficulty()); - BubbleSprite malusBubble = new BubbleSprite( - new Rect(columnX[i], 44+15*28, 32, 32), - START_LAUNCH_DIRECTION, - color, bubbles[color], bubblesBlind[color], - frozenBubbles[color], targetedBubbles, bubbleBlink, - bubbleManager, soundManager, this); - goingUp.add(malusBubble); - this.addSprite(malusBubble); - } - } + if (playSound) { + soundManager.playSound(FrozenBubble.SOUND_NEWROOT); } - } - - private void sendBubblesDown() { - soundManager.playSound(FrozenBubble.SOUND_NEWROOT); - for (int i=0 ; i<8 ; i++) { - for (int j=0 ; j<12 ; j++) { + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 12; j++) { if (bubblePlay[i][j] != null) { bubblePlay[i][j].moveDown(); - if ((bubblePlay[i][j].getSpritePosition().y>=380) && !endOfGame) { + if ((bubblePlay[i][j].getSpritePosition().y >= 380) && !endOfGame) { penguin.updateState(PenguinSprite.STATE_GAME_LOST); if (highscoreManager != null) highscoreManager.lostLevel(); - playResult = GAME_LOST; + playResult = gameEnum.LOST; endOfGame = true; initFrozenify(); - soundManager.playSound(FrozenBubble.SOUND_LOST); + if (playSound) { + soundManager.playSound(FrozenBubble.SOUND_LOST); + } } } } } - moveDown += 28.; compressor.moveDown(); } - public int play(boolean key_left, boolean key_right, - boolean key_fire, boolean key_swap, - double trackball_dx, - boolean touch_fire, double touch_x, double touch_y, - boolean ats_touch_fire, double ats_touch_dx) { + /** + * Move the launched bubble. + * @return true if the compressor was lowered. + */ + public boolean manageMovingBubble() { + boolean compressed = false; + + if (movingBubble != null) { + movingBubble.move(); + if (movingBubble.fixed()) { + if (!checkLost()) { + if (bubbleManager.countBubbles() == 0) { + penguin.updateState(PenguinSprite.STATE_GAME_WON); + this.addSprite(new ImageSprite(new Rect(152, 190, + 152 + 337, + 190 + 116), gameWon)); + if (highscoreManager != null) + highscoreManager.endLevel(nbBubbles); + playResult = gameEnum.WON; + endOfGame = true; + soundManager.playSound(FrozenBubble.SOUND_WON); + } + else if ((malusBar == null) || FrozenBubble.getCompressor()) { + fixedBubbles++; + blinkDelay = 0; + + if ((fixedBubbles == 8) && !isRemote) { + lowerCompressor(true); + compressed = true; + } + } + } + movingBubble = null; + } + } + return compressed; + } + + public void paint(Canvas c, double scale, int dx, int dy) { + compressor.paint(c, scale, dx, dy); + if (FrozenBubble.getMode() == FrozenBubble.GAME_NORMAL) { + nextBubble.changeImage(bubbles[nextColor]); + } + else { + nextBubble.changeImage(bubblesBlind[nextColor]); + } + super.paint(c, scale, dx, dy); + } + + public void pause() { + this.removeSprite(pausedSprite); + this.addSprite(pausedSprite); + } + + public void pauseButtonPressed(boolean paused) { + if (paused) { + this.removeSprite(pauseButtonSprite); + this.removeSprite(playButtonSprite); + this.addSprite(playButtonSprite); + } + else { + this.removeSprite(pauseButtonSprite); + this.removeSprite(playButtonSprite); + this.addSprite(pauseButtonSprite); + } + } + + public gameEnum play(boolean key_left, boolean key_right, + boolean key_fire, boolean key_swap, + double trackball_dx, + boolean touch_fire, double touch_x, double touch_y, + boolean ats_touch_fire, double ats_touch_dx) { boolean ats = FrozenBubble.getAimThenShoot(); + boolean bubbleLaunched = false; + boolean compressed = false; + int[] move = new int[2]; + int attackBarBubbles = 0; + int currentColorWas = currentColor; + int nextColorWas = nextColor; + int numAttackBubbles = 0; + + if (malusBar != null) { + sendToOpponent = 0; + attackBarBubbles = malusBar.getAttackBarBubbles(); + } if ((ats && ats_touch_fire) || (!ats && touch_fire)) { key_fire = true; } - int[] move = new int[2]; - if (key_left && !key_right) { move[HORIZONTAL_MOVE] = KEY_LEFT; } @@ -857,7 +679,7 @@ else if (key_right && !key_left) { swapPressed = false; } - if (!ats && touch_fire && movingBubble == null) { + if (!ats && touch_fire && !isRemote && (movingBubble == null)) { double xx = touch_x - 318; double yy = 406 - touch_y; launchBubblePosition = (Math.PI - Math.atan2(yy, xx)) * 40.0 / Math.PI; @@ -868,34 +690,42 @@ else if (key_right && !key_left) { readyToFire = true; } - if (FrozenBubble.getDontRushMe()) { + /* + * If the option to rush the player is disabled or this game + * represents the remote player in a network game, initialize + * hurryTime to disable automatic bubbles launches. + */ + if (FrozenBubble.getDontRushMe() || isRemote) { hurryTime = 1; } if (endOfGame && readyToFire) { if (move[FIRE] == KEY_UP) { - if (playResult == GAME_WON) { + if (playResult == gameEnum.WON) { levelManager.goToNextLevel(); - playResult = GAME_NEXT_WON; + playResult = gameEnum.NEXT_WON; + } + else { + playResult = gameEnum.NEXT_LOST; } - else - playResult = GAME_NEXT_LOST; - return playResult; } else { penguin.updateState(PenguinSprite.STATE_VOID); - if (frozenify) { + /* + * If the game is over because of bubble overflow, wait until + * all the bubbles are fixed in place to freeze them. + */ + if (frozenify && (goingUp.size() == 0)) { frozenify(); } } } else { if ((move[FIRE] == KEY_UP) || (hurryTime > HURRY_ME_TIME)) { - if ((movingBubble == null) && readyToFire) { + if (getOkToFire()) { nbBubbles++; - movingBubble = new BubbleSprite(new Rect(302, 390, 32, 32), launchBubblePosition, currentColor, @@ -905,9 +735,15 @@ else if (key_right && !key_left) { targetedBubbles, bubbleBlink, bubbleManager, soundManager, this); this.addSprite(movingBubble); - + bubbleLaunched = true; currentColor = nextColor; - nextColor = bubbleManager.nextBubbleIndex(random); + + if (isRemote) { + nextColor = newNextColor; + } + else { + nextColor = bubbleManager.nextBubbleIndex(random); + } if (FrozenBubble.getMode() == FrozenBubble.GAME_NORMAL) { nextBubble.changeImage(bubbles[nextColor]); @@ -915,13 +751,17 @@ else if (key_right && !key_left) { else { nextBubble.changeImage(bubblesBlind[nextColor]); } + launchBubble.changeColor(currentColor); penguin.updateState(PenguinSprite.STATE_FIRE); soundManager.playSound(FrozenBubble.SOUND_LAUNCH); readyToFire = false; hurryTime = 0; - if (malusBar != null) + + if (malusBar != null) { malusBar.releaseTime = RELEASE_TIME; + } + removeSprite(hurrySprite); } else { @@ -947,43 +787,43 @@ else if (key_right && !key_left) { } } - sendToOpponent = 0; /* * The moving bubble is moved twice, which produces smoother * animation. Thus the moving bubble effectively moves at twice the * animation speed with respect to other bubbles that are only * moved once per iteration. */ - manageMovingBubble(); - manageMovingBubble(); + compressed = manageMovingBubble(); + compressed |= manageMovingBubble(); - if (movingBubble == null && !endOfGame) { + if ((movingBubble == null) && !endOfGame) { hurryTime++; if (malusBar != null) malusBar.releaseTime++; - // If hurryTime == 2 (1 + 1) we could be in the "Don't rush me" - // mode. Remove the sprite just in case the user switched - // to this mode when the "Hurry" sprite was shown, to make it - // disappear. + /* + * If hurryTime == 2 (1 + 1) we could be in the "Don't rush me" + * mode. Remove the sprite just in case the user switched + * to this mode when the "Hurry" sprite was shown, to make it + * disappear. + */ if (hurryTime == 2) { removeSprite(hurrySprite); } - if (hurryTime>=240) { - if (hurryTime % 40 == 10) { + if (hurryTime >= 240) { + if (hurryTime%40 == 10) { addSprite(hurrySprite); soundManager.playSound(FrozenBubble.SOUND_HURRY); } - else if (hurryTime % 40 == 35) { + else if (hurryTime%40 == 35) { removeSprite(hurrySprite); } } if (malusBar != null) { - if (malusBar.releaseTime > RELEASE_TIME) { - releaseBubbles(); + if (getOkToFire() && (attackBarBubbles > 0) && + ((malusBar.releaseTime > RELEASE_TIME) || isRemote)) { + numAttackBubbles = releaseBubbles(); malusBar.releaseTime = 0; } - - checkLost(); } } @@ -992,7 +832,6 @@ else if (hurryTime % 40 == 35) { if (blinkDelay < 15) { blinkLine(blinkDelay); } - blinkDelay++; if (blinkDelay == 40) { blinkDelay = 0; @@ -1002,7 +841,6 @@ else if (fixedBubbles == 7) { if (blinkDelay < 15) { blinkLine(blinkDelay); } - blinkDelay++; if (blinkDelay == 25) { blinkDelay = 0; @@ -1010,142 +848,477 @@ else if (fixedBubbles == 7) { } } - for (int i=0 ; i 0)) { + if (bubbleLaunched || (numAttackBubbles > 0)) { + gridChecksum = 0; + } + if (!isRemote) { + networkManager.sendLocalPlayerAction(player, + compressed, + bubbleLaunched, + swapPressed, + 0, + currentColorWas, + nextColorWas, + nextColor, + attackBarBubbles, + malusBar.attackBubbles, + launchBubblePosition); + } + } + else if ((gridChecksum == 0) && getOkToFire()) { + calculateGridChecksum(); + } + } + + if (malusBar != null) { + malusBar.clearAttackBubbles(); + } + + return gameEnum.PLAYING; } - public void paint(Canvas c, double scale, int dx, int dy) { - compressor.paint(c, scale, dx, dy); - if (FrozenBubble.getMode() == FrozenBubble.GAME_NORMAL) { - nextBubble.changeImage(bubbles[nextColor]); + /** + * Populate random columns in a row of attack bubbles to launch onto + * the game field. + *

In an actual play field, the rows alternate between a maximum 7 + * and 8 bubbles per row. Thus 7 bubbles are sent up as that is the + * maximum number of bubbles that can fit in each alternating row. + *

There are 15 distinct positions ("lanes") for bubbles to occupy + * between two consecutive rows. Thus we send up a maximum 7 bubbles + * in randomly selected "lanes" from the 15 available. + * @return The number of attack bubbles launched. + */ + private int releaseBubbles() { + if (malusBar == null) { + return 0; } - else { - nextBubble.changeImage(bubblesBlind[nextColor]); + + int numBubblesLaunched = 0; + + /* + * If this game represents a remote player, the the attack bubbles + * are calculated on the remote machine and sent over the network. + * Simply use the supplied attack bubble buffer to initiate attack + * bubble launches. + */ + if (isRemote) { + for (int i = 0; i < 15; i++) { + if (malusBar.attackBubbles[i] >= 0) { + numBubblesLaunched++; + int color = malusBar.attackBubbles[i]; + BubbleSprite malusBubble = new BubbleSprite( + new Rect(columnX[i], 44+15*28, 32, 32), + START_LAUNCH_DIRECTION, + color, bubbles[color], bubblesBlind[color], + frozenBubbles[color], targetedBubbles, bubbleBlink, + bubbleManager, soundManager, this); + goingUp.add(malusBubble); + this.addSprite(malusBubble); + } + } + malusBar.removeAttackBubbles(numBubblesLaunched); } - super.paint(c, scale, dx, dy); - } + else if (malusBar.getAttackBarBubbles() > 0) { + boolean[] lanes = new boolean[15]; + int malusBalls = malusBar.removeLine(); + int pos; - public int getCompressorPosition() { - return compressor.steps; - } + while (malusBalls > 0) { + pos = random.nextInt(15); + if (!lanes[pos]) { + lanes[pos] = true; + malusBalls--; + } + } - public int getCurrentColor() { - return currentColor; - } + for (int i = 0; i < 15; i++) { + if (lanes[i]) { + numBubblesLaunched++; + int color = random.nextInt(FrozenBubble.getDifficulty()); + malusBar.setAttackBubble(i, color); + BubbleSprite malusBubble = new BubbleSprite( + new Rect(columnX[i], 44+15*28, 32, 32), + START_LAUNCH_DIRECTION, + color, bubbles[color], bubblesBlind[color], + frozenBubbles[color], targetedBubbles, bubbleBlink, + bubbleManager, soundManager, this); + goingUp.add(malusBubble); + this.addSprite(malusBubble); + } + } + } - public int getGameResult() { - return playResult; + return numBubblesLaunched; } - public int getNextColor() { - return nextColor; + private Sprite restoreSprite(Bundle map, Vector imageList, int i) { + int left = map.getInt(String.format("%d-%d-left", player, i)); + int right = map.getInt(String.format("%d-%d-right", player, i)); + int top = map.getInt(String.format("%d-%d-top", player, i)); + int bottom = map.getInt(String.format("%d-%d-bottom", player, i)); + int type = map.getInt(String.format("%d-%d-type", player, i)); + if (type == Sprite.TYPE_BUBBLE) { + int color = map.getInt(String.format("%d-%d-color", player, i)); + double moveX = map.getDouble(String.format("%d-%d-moveX", player, i)); + double moveY = map.getDouble(String.format("%d-%d-moveY", player, i)); + double realX = map.getDouble(String.format("%d-%d-realX", player, i)); + double realY = map.getDouble(String.format("%d-%d-realY", player, i)); + boolean fixed = map.getBoolean(String.format("%d-%d-fixed", player, i)); + boolean blink = map.getBoolean(String.format("%d-%d-blink", player, i)); + boolean released = + map.getBoolean(String.format("%d-%d-released", player, i)); + boolean checkJump = + map.getBoolean(String.format("%d-%d-checkJump", player, i)); + boolean checkFall = + map.getBoolean(String.format("%d-%d-checkFall", player, i)); + int fixedAnim = map.getInt(String.format("%d-%d-fixedAnim", player, i)); + boolean frozen = + map.getBoolean(String.format("%d-%d-frozen", player, i)); + Point lastOpenPosition = new Point( + map.getInt(String.format("%d-%d-lastOpenPosition.x", player, i)), + map.getInt(String.format("%d-%d-lastOpenPosition.y", player, i))); + return new BubbleSprite(new Rect(left, top, right, bottom), + color, moveX, moveY, realX, realY, + fixed, blink, released, checkJump, checkFall, + fixedAnim, + (frozen ? frozenBubbles[color] : bubbles[color]), + lastOpenPosition, + bubblesBlind[color], + frozenBubbles[color], + targetedBubbles, bubbleBlink, + bubbleManager, soundManager, this); + } + else if (type == Sprite.TYPE_IMAGE) { + int imageId = map.getInt(String.format("%d-%d-imageId", player, i)); + return new ImageSprite(new Rect(left, top, right, bottom), + (BmpWrap)imageList.elementAt(imageId)); + } + else if (type == Sprite.TYPE_LAUNCH_BUBBLE) { + int currentColor = + map.getInt(String.format("%d-%d-currentColor", player, i)); + double currentDirection = + map.getDouble(String.format("%d-%d-currentDirection", player, i)); + return new LaunchBubbleSprite(currentColor, currentDirection, + launcher, bubbles, bubblesBlind); + } + else if (type == Sprite.TYPE_PENGUIN) { + int currentPenguin = + map.getInt(String.format("%d-%d-currentPenguin", player, i)); + int count = map.getInt(String.format("%d-%d-count", player, i)); + int finalState = + map.getInt(String.format("%d-%d-finalState", player, i)); + int nextPosition = + map.getInt(String.format("%d-%d-nextPosition", player, i)); + + return new PenguinSprite(getPenguinRect(player), penguins, random, + currentPenguin, count, finalState, + nextPosition); + } + else { + Log.e("frozen-bubble", "Unrecognized sprite type: " + type); + return null; + } } - public boolean getOkToFire() { - return ((movingBubble == null) && (playResult == GAME_PLAYING) && - readyToFire); + public void restoreState(Bundle map, Vector imageList) { + Vector savedSprites = new Vector(); + int numSavedSprites = + map.getInt(String.format("%d-numSavedSprites", player)); + for (int i = 0; i < numSavedSprites; i++) { + savedSprites.addElement(restoreSprite(map, imageList, i)); + } + + restoreSprites(map, savedSprites, player); + + jumping = new Vector(); + int numJumpingSprites = + map.getInt(String.format("%d-numJumpingSprites", player)); + for (int i = 0; i < numJumpingSprites; i++) { + int spriteIdx = map.getInt(String.format("%d-jumping-%d", player, i)); + jumping.addElement(savedSprites.elementAt(spriteIdx)); + } + goingUp = new Vector(); + int numGoingUpSprites = + map.getInt(String.format("%d-numGoingUpSprites", player)); + for (int i = 0; i < numGoingUpSprites; i++) { + int spriteIdx = map.getInt(String.format("%d-goingUp-%d", player, i)); + goingUp.addElement(savedSprites.elementAt(spriteIdx)); + } + falling = new Vector(); + int numFallingSprites = + map.getInt(String.format("%d-numFallingSprites", player)); + for (int i = 0; i < numFallingSprites; i++) { + int spriteIdx = map.getInt(String.format("%d-falling-%d", player, i)); + falling.addElement(savedSprites.elementAt(spriteIdx)); + } + bubblePlay = new BubbleSprite[8][13]; + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 13; j++) { + int spriteIdx = + map.getInt(String.format("%d-play-%d-%d", player, i, j)); + if (spriteIdx != -1) { + bubblePlay[i][j] = (BubbleSprite)savedSprites.elementAt(spriteIdx); + } + else { + bubblePlay[i][j] = null; + } + } + } + int launchBubbleId = + map.getInt(String.format("%d-launchBubbleId", player)); + launchBubble = (LaunchBubbleSprite)savedSprites.elementAt(launchBubbleId); + launchBubblePosition = + map.getDouble(String.format("%d-launchBubblePosition", player)); + if (malusBar != null) { + malusBar.restoreState(map, player); + this.addSprite(malusBar); + } + int penguinId = map.getInt(String.format("%d-penguinId", player)); + penguin = (PenguinSprite)savedSprites.elementAt(penguinId); + compressor.restoreState(map, player); + int nextBubbleId = map.getInt(String.format("%d-nextBubbleId", player)); + nextBubble = (ImageSprite)savedSprites.elementAt(nextBubbleId); + currentColor = map.getInt(String.format("%d-currentColor", player)); + nextColor = map.getInt(String.format("%d-nextColor", player)); + int movingBubbleId = + map.getInt(String.format("%d-movingBubbleId", player)); + if (movingBubbleId == -1) { + movingBubble = null; + } + else { + movingBubble = (BubbleSprite)savedSprites.elementAt(movingBubbleId); + } + bubbleManager.restoreState(map, player); + fixedBubbles = map.getInt(String.format("%d-fixedBubbles", player)); + nbBubbles = map.getInt(String.format("%d-nbBubbles", player)); + sendToOpponent = map.getInt(String.format("%d-sendToOpponent", player)); + blinkDelay = map.getInt(String.format("%d-blinkDelay", player)); + int hurryId = map.getInt(String.format("%d-hurryId", player)); + hurrySprite = (ImageSprite)savedSprites.elementAt(hurryId); + hurryTime = map.getInt(String.format("%d-hurryTime", player)); + int pausedId = map.getInt(String.format("%d-pausedId", player)); + pausedSprite = (ImageSprite)savedSprites.elementAt(pausedId); + readyToFire = map.getBoolean(String.format("%d-readyToFire", player)); + endOfGame = map.getBoolean(String.format("%d-endOfGame", player)); + frozenify = map.getBoolean(String.format("%d-frozenify", player)); + frozenifyX = map.getInt(String.format("%d-frozenifyX", player)); + frozenifyY = map.getInt(String.format("%d-frozenifyY", player)); } - public double getPosition() { - return launchBubblePosition; + public void resume() { + this.removeSprite(pausedSprite); } - public void manageMovingBubble() { - if (movingBubble != null) { - movingBubble.move(); - if (movingBubble.fixed()) { - if (!checkLost()) { - if (bubbleManager.countBubbles() == 0) { - penguin.updateState(PenguinSprite.STATE_GAME_WON); - this.addSprite(new ImageSprite(new Rect(152, 190, - 152 + 337, - 190 + 116), gameWon)); - if (highscoreManager != null) - highscoreManager.endLevel(nbBubbles); - playResult = GAME_WON; - endOfGame = true; - soundManager.playSound(FrozenBubble.SOUND_WON); - } - else if ((malusBar == null) || FrozenBubble.getCompressor()) { - fixedBubbles++; - blinkDelay = 0; + public void saveState(Bundle map) { + Vector savedSprites = new Vector(); - if (fixedBubbles == 8) { - fixedBubbles = 0; - sendBubblesDown(); - } - } + saveSprites(map, savedSprites, player); + + for (int i = 0; i < jumping.size(); i++) { + ((Sprite)jumping.elementAt(i)).saveState(map, savedSprites, player); + map.putInt(String.format("%d-jumping-%d", player, i), + ((Sprite)jumping.elementAt(i)).getSavedId()); + } + map.putInt(String.format("%d-numJumpingSprites", player), jumping.size()); + for (int i = 0; i < goingUp.size(); i++) { + ((Sprite)goingUp.elementAt(i)).saveState(map, savedSprites, player); + map.putInt(String.format("%d-goingUp-%d", player, i), + ((Sprite)goingUp.elementAt(i)).getSavedId()); + } + map.putInt(String.format("%d-numGoingUpSprites", player), goingUp.size()); + for (int i = 0; i < falling.size(); i++) { + ((Sprite)falling.elementAt(i)).saveState(map, savedSprites, player); + map.putInt(String.format("%d-falling-%d", player, i), + ((Sprite)falling.elementAt(i)).getSavedId()); + } + map.putInt(String.format("%d-numFallingSprites", player), falling.size()); + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 13; j++) { + if (bubblePlay[i][j] != null) { + bubblePlay[i][j].saveState(map, savedSprites, player); + map.putInt(String.format("%d-play-%d-%d", player, i, j), + bubblePlay[i][j].getSavedId()); + } + else { + map.putInt(String.format("%d-play-%d-%d", player, i, j), -1); } - movingBubble = null; } } + launchBubble.saveState(map, savedSprites, player); + map.putInt(String.format("%d-launchBubbleId", player), + launchBubble.getSavedId()); + map.putDouble(String.format("%d-launchBubblePosition", player), + launchBubblePosition); + if (malusBar != null) { + malusBar.saveState(map, player); + } + penguin.saveState(map, savedSprites, player); + compressor.saveState(map, player); + map.putInt(String.format("%d-penguinId", player), penguin.getSavedId()); + nextBubble.saveState(map, savedSprites, player); + map.putInt(String.format("%d-nextBubbleId", player), + nextBubble.getSavedId()); + map.putInt(String.format("%d-currentColor", player), currentColor); + map.putInt(String.format("%d-nextColor", player), nextColor); + if (movingBubble != null) { + movingBubble.saveState(map, savedSprites, player); + map.putInt(String.format("%d-movingBubbleId", player), + movingBubble.getSavedId()); + } + else { + map.putInt(String.format("%d-movingBubbleId", player), -1); + } + bubbleManager.saveState(map, player); + map.putInt(String.format("%d-fixedBubbles", player), fixedBubbles); + map.putInt(String.format("%d-nbBubbles", player), nbBubbles); + map.putInt(String.format("%d-sendToOpponent", player), sendToOpponent); + map.putInt(String.format("%d-blinkDelay", player), blinkDelay); + hurrySprite.saveState(map, savedSprites, player); + map.putInt(String.format("%d-hurryId", player), hurrySprite.getSavedId()); + map.putInt(String.format("%d-hurryTime", player), hurryTime); + pausedSprite.saveState(map, savedSprites, player); + map.putInt(String.format("%d-pausedId", player), pausedSprite.getSavedId()); + map.putBoolean(String.format("%d-readyToFire", player), readyToFire); + map.putBoolean(String.format("%d-endOfGame", player), endOfGame); + map.putBoolean(String.format("%d-frozenify", player), frozenify); + map.putInt(String.format("%d-frozenifyX", player), frozenifyX); + map.putInt(String.format("%d-frozenifyY", player), frozenifyY); + map.putInt(String.format("%d-numSavedSprites", player), + savedSprites.size()); + for (int i = 0; i < savedSprites.size(); i++) { + ((Sprite)savedSprites.elementAt(i)).clearSavedId(); + } } - public void setGameResult(int result) { - playResult = result; - endOfGame = true; + /** + * Lower the compressor to the specified number of steps. + * @param steps - the number of compressor steps to lower to. + */ + public void setCompressorSteps(byte steps) { + byte stepsNow = (byte) compressor.getSteps(); - if (result == GAME_WON) - { - penguin.updateState(PenguinSprite.STATE_GAME_WON); - this.addSprite(new ImageSprite(new Rect(152, 190, - 152 + 337, - 190 + 116), gameWon)); + if ((steps < 0) || (steps > 13)) { + return; } - else if (result == GAME_LOST) - { - penguin.updateState(PenguinSprite.STATE_GAME_LOST); - this.addSprite(new ImageSprite(new Rect(152, 190, - 152 + 337, - 190 + 116), gameLost)); + + if (steps > stepsNow) { + stepsNow = (byte) (steps - stepsNow); + while (stepsNow-- > 0) { + lowerCompressor(false); + } } } - public void clampLaunchPosition() { - if (launchBubblePosition < MIN_LAUNCH_DIRECTION) { - launchBubblePosition = MIN_LAUNCH_DIRECTION; + /** + * Set the game result associated with this player. + * @param result - GAME_WON if this player won the game, GAME_LOST if + * this player lost the game. + */ + public void setGameResult(gameEnum result) { + if (!endOfGame) { + playResult = result; + if (result == gameEnum.WON) + { + penguin.updateState(PenguinSprite.STATE_GAME_WON); + this.addSprite(new ImageSprite(new Rect(152, 190, + 152 + 337, + 190 + 116), gameWon)); + } + else if (result == gameEnum.LOST) + { + penguin.updateState(PenguinSprite.STATE_GAME_LOST); + this.addSprite(new ImageSprite(new Rect(152, 190, + 152 + 337, + 190 + 116), gameLost)); + } + endOfGame = true; } - if (launchBubblePosition > MAX_LAUNCH_DIRECTION) { - launchBubblePosition = MAX_LAUNCH_DIRECTION; + } + + public void setGrid(byte[][] newGrid) { + bubbleManager.initialize(); + removeAllBubbleSprites(); + falling.clear(); + goingUp.clear(); + jumping.clear(); + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 13; j++) { + bubblePlay[i][j] = null; + if (newGrid[i][j] != -1) { + bubblePlay[i][j] = new BubbleSprite( + new Rect(190+i*32-(j%2)*16, 44+j*28, 32, 32), + newGrid[i][j], + bubbles[newGrid[i][j]], bubblesBlind[newGrid[i][j]], + frozenBubbles[newGrid[i][j]], bubbleBlink, bubbleManager, + soundManager, this); + this.addSprite(bubblePlay[i][j]); + } + } } } - public void setPosition(double value) { - double dx = value - launchBubblePosition; - /* - * For small changes, don't update the penguin state. - */ - if ((dx < 0.25) && (dx > -0.25)) - dx = 0; - launchBubblePosition = value; - clampLaunchPosition(); - launchBubble.changeDirection(launchBubblePosition); - updatePenguinState(dx); + public void setLaunchBubbleColors(int current, int next, int newNext) { + currentColor = current; + nextColor = next; + newNextColor = newNext; + launchBubble.changeColor(currentColor); + + if (FrozenBubble.getMode() == FrozenBubble.GAME_NORMAL) + nextBubble.changeImage(bubbles[nextColor]); + else + nextBubble.changeImage(bubblesBlind[nextColor]); } - public void setSendToOpponent(int numAttackBubbles) { - sendToOpponent = numAttackBubbles; - } + public void setPosition(double value) { + if (!endOfGame) { + double dx = value - launchBubblePosition; + /* + * For small position changes, don't update the penguin state. + */ + if ((dx < 0.25) && (dx > -0.25)) + dx = 0; + launchBubblePosition = value; + clampLaunchPosition(); + launchBubble.changeDirection(launchBubblePosition); + updatePenguinState(dx); + } + } public void swapNextLaunchBubble() { if (currentColor != nextColor) { int tempColor = currentColor; currentColor = nextColor; nextColor = tempColor; - launchBubble.changeColor(currentColor); if (FrozenBubble.getMode() == FrozenBubble.GAME_NORMAL) @@ -1157,6 +1330,64 @@ public void swapNextLaunchBubble() { } } + /** + * This function is an unfortunate patch that is necessitated due to + * the fact that there is as of yet an unfixed bug in the BubbleSprite + * management code. + *

Somewhere amongst goUp() and move() in BubbleSprite.java, a flaw + * exists whereby a bubble is added to the bubble manager, and the + * bubble sprite is added to the game screen, but the entry in the + * bubblePlay grid was either rendered null or a bubble superposition + * in the grid occurred. The former is suspected, because ensuring + * the grid location is null before assigning a bubble sprite to it is + * very rigorously enforced. + *

TODO - fix the grid entry bug. + */ + public void synchronizeBubbleManager() { + int numBubblesManager = bubbleManager.countBubbles(); + int numBubblesPlay = 0; + /* + * Check the bubble sprite grid for occupied locations. + */ + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 13; j++) { + if (bubblePlay[i][j] != null ) { + numBubblesPlay++; + } + } + } + /* + * If the number of bubble sprite grid entries does not match the + * number of bubbles in the bubble manager, then we need to re- + * initialize the bubble manager, and re-initialize all the bubble + * sprites on the game screen. You would be unable to win prior to + * the addition of this synchronization code due to the number of + * bubbles in the bubble manager never reaching zero, and the excess + * sprite or sprites would remain stuck on the screen. + */ + if (numBubblesManager != numBubblesPlay) { + bubbleManager.initialize(); + removeAllBubbleSprites(); + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 13; j++) { + if (bubblePlay[i][j] != null ) { + bubblePlay[i][j].addToManager(); + this.addSprite(bubblePlay[i][j]); + } + } + } + for (int i = 0; i < falling.size(); i++) { + this.addSprite(falling.elementAt(i)); + } + for (int i = 0; i < goingUp.size(); i++) { + this.addSprite(goingUp.elementAt(i)); + } + for (int i = 0; i < jumping.size(); i++) { + this.addSprite(jumping.elementAt(i)); + } + } + } + public void updatePenguinState(double dx) { if (dx < 0) { penguin.updateState(PenguinSprite.STATE_TURN_LEFT); diff --git a/src/org/jfedor/frozenbubble/GameScreen.java b/src/org/jfedor/frozenbubble/GameScreen.java index a7cbab4..c299e6d 100644 --- a/src/org/jfedor/frozenbubble/GameScreen.java +++ b/src/org/jfedor/frozenbubble/GameScreen.java @@ -58,6 +58,29 @@ import android.os.Bundle; public abstract class GameScreen { + + public static enum eventEnum { + GAME_WON, + GAME_LOST, + GAME_PAUSED, + GAME_RESUME, + LEVEL_START; + } + + public static enum gameEnum { + PLAYING, + LOST, + WON, + NEXT_LOST, + NEXT_WON; + } + + public static enum stateEnum { + RUNNING, + PAUSED, + ABOUT; + } + private Vector sprites; public final void saveSprites(Bundle map, Vector savedSprites, @@ -120,10 +143,10 @@ public void paint(Canvas c, double scale, int dx, int dy) { } } - public abstract int play(boolean key_left, boolean key_right, - boolean key_fire, boolean key_swap, - double trackball_dx, - boolean touch_fire, - double touch_x, double touch_y, - boolean ats_touch_fire, double ats_touch_dx); + public abstract gameEnum play(boolean key_left, boolean key_right, + boolean key_fire, boolean key_swap, + double trackball_dx, + boolean touch_fire, + double touch_x, double touch_y, + boolean ats_touch_fire, double ats_touch_dx); } diff --git a/src/org/jfedor/frozenbubble/GameView.java b/src/org/jfedor/frozenbubble/GameView.java index 991f505..1af3779 100644 --- a/src/org/jfedor/frozenbubble/GameView.java +++ b/src/org/jfedor/frozenbubble/GameView.java @@ -83,6 +83,10 @@ import java.util.TimerTask; import java.util.Vector; +import org.jfedor.frozenbubble.GameScreen.eventEnum; +import org.jfedor.frozenbubble.GameScreen.gameEnum; +import org.jfedor.frozenbubble.GameScreen.stateEnum; + import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -111,19 +115,18 @@ class GameView extends SurfaceView implements SurfaceHolder.Callback { private boolean mBlankScreen = false; private Context mContext; private GameThread mGameThread; - //********************************************************** + + //******************************************************************** // Listener interface for various events - //********************************************************** - // Event types. - public static final int EVENT_GAME_WON = 2; - public static final int EVENT_GAME_LOST = 3; - public static final int EVENT_GAME_PAUSED = 4; - public static final int EVENT_GAME_RESUME = 5; - public static final int EVENT_LEVEL_START = 6; - - // Listener user set. + //******************************************************************** + + /** + * Game event listener user set. + * @author Eric Fortin + * + */ public interface GameListener { - public abstract void onGameEvent(int event); + public abstract void onGameEvent(eventEnum event); } GameListener mGameListener; @@ -136,10 +139,6 @@ class GameThread extends Thread { private static final int FRAME_DELAY = 40; - public static final int STATE_RUNNING = 1; - public static final int STATE_PAUSE = 2; - public static final int STATE_ABOUT = 4; - private static final double TRACKBALL_COEFFICIENT = 5; private static final double TOUCH_FIRE_Y_THRESHOLD = 380; private static final double TOUCH_SWAP_X_THRESHOLD = 14; @@ -173,8 +172,9 @@ class GameThread extends Thread { private int mDisplayDY; private double mDisplayScale; private long mLastTime; - private int mMode; - private int mModeWas; + + private stateEnum mMode; + private stateEnum mModeWas; private Bitmap mBackgroundOrig; private Bitmap[] mBubblesOrig; @@ -236,7 +236,7 @@ public GameThread(SurfaceHolder surfaceHolder, byte[] customLevels, //Log.i("frozen-bubble", "GameThread()"); mSurfaceHolder = surfaceHolder; Resources res = mContext.getResources(); - setState(STATE_PAUSE); + setState(stateEnum.PAUSED); BitmapFactory.Options options = new BitmapFactory.Options(); @@ -451,11 +451,11 @@ private void resizeBitmaps() { public void pause() { synchronized (mSurfaceHolder) { - if (mMode == STATE_RUNNING) { - setState(STATE_PAUSE); + if (mMode == stateEnum.RUNNING) { + setState(stateEnum.PAUSED); if (mGameListener != null) - mGameListener.onGameEvent(EVENT_GAME_PAUSED); + mGameListener.onGameEvent(eventEnum.GAME_PAUSED); if (mFrozenGame != null) mFrozenGame.pause(); if (mHighscoreManager != null) @@ -466,7 +466,7 @@ public void pause() { public void resumeGame() { synchronized (mSurfaceHolder) { - if (mMode == STATE_RUNNING) { + if (mMode == stateEnum.RUNNING) { mFrozenGame .resume(); mHighscoreManager.resumeLevel(); } @@ -505,22 +505,22 @@ public void run() { if (c != null) { synchronized (mSurfaceHolder) { if (mRun) { - if (mMode == STATE_ABOUT) { + if (mMode == stateEnum.ABOUT) { drawAboutScreen(c); } - else if (mMode == STATE_PAUSE) { + else if (mMode == stateEnum.PAUSED) { if (mShowScores) drawHighScoreScreen(c, mHighscoreManager.getLevel()); else doDraw(c); } else { - if (mMode == STATE_RUNNING) { - if (mModeWas != STATE_RUNNING) { + if (mMode == stateEnum.RUNNING) { + if (mModeWas != stateEnum.RUNNING) { if (mGameListener != null) - mGameListener.onGameEvent(EVENT_GAME_RESUME); + mGameListener.onGameEvent(eventEnum.GAME_RESUME); - mModeWas = STATE_RUNNING; + mModeWas = stateEnum.RUNNING; resumeGame(); } updateGameState(); @@ -544,7 +544,6 @@ else if (mMode == STATE_PAUSE) { /** * Dump game state to the provided Bundle. Typically called when the * Activity is being suspended. - * * @return Bundle with this view's state */ public Bundle saveState(Bundle map) { @@ -560,16 +559,14 @@ public Bundle saveState(Bundle map) { } /** - * Restores game state from the indicated Bundle. Typically called when - * the Activity is being restored after having been previously + * Restores game state from the indicated Bundle. Typically called + * when the Activity is being restored after having been previously * destroyed. - * - * @param savedState - * - Bundle containing the game state. + * @param savedState - Bundle containing the game state. */ public synchronized void restoreState(Bundle map) { synchronized (mSurfaceHolder) { - setState(STATE_PAUSE); + setState(stateEnum.PAUSED); mFrozenGame .restoreState(map, mImageList); mLevelManager .restoreState(map); mHighscoreManager.restoreState(map); @@ -580,23 +577,22 @@ public void setRunning(boolean b) { mRun = b; } - public void setState(int mode) { + public void setState(stateEnum newMode) { synchronized (mSurfaceHolder) { - // - // Only update the previous mode storage if the new mode is - // different from the current mode, in case the same mode is - // being set multiple times. - // - // The transition from state to state must be preserved in - // case a separate execution thread that checks for state - // transitions does not get a chance to run between calls to - // this method. - // - // - if (mode != mMode) + /* + * Only update the previous mode storage if the new mode is + * different from the current mode, in case the same mode is + * being set multiple times. + * + * The transition from state to state must be preserved in + * case a separate execution thread that checks for state + * transitions does not get a chance to run between calls to + * this method. + */ + if (newMode != mMode) mModeWas = mMode; - mMode = mode; + mMode = newMode; } } @@ -636,13 +632,9 @@ public void setSurfaceSize(int width, int height) { /** * Process key presses. This must be allowed to run regardless of * the game state to correctly handle initial game conditions. - * - * @param keyCode - * - the static KeyEvent key identifier. - * @param msg - * - the key action message. - * @return - * - true if the key action is processed, false if not. + * @param keyCode - the static KeyEvent key identifier. + * @param msg - the key action message. + * @return true if the key action is processed. * @see android.view.View#onKeyDown(int, android.view.KeyEvent) */ boolean doKeyDown(int keyCode, KeyEvent msg) { @@ -690,15 +682,9 @@ else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { /** * Process key releases. This must be allowed to run regardless of * the game state in order to properly clear key presses. - * - * @param keyCode - * - the static KeyEvent key identifier. - * - * @param msg - * - the key action message. - * - * @return true if the key action is processed, false if not. - * + * @param keyCode - the static KeyEvent key identifier. + * @param msg - the key action message. + * @return true if the key action is processed. * @see android.view.View#onKeyUp(int, android.view.KeyEvent) */ boolean doKeyUp(int keyCode, KeyEvent msg) { @@ -729,23 +715,18 @@ else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { /** * Process trackball motion events. - *

- * This method only processes trackball motion for the purpose of + *

This method only processes trackball motion for the purpose of * aiming the launcher. The trackball has no effect on the game * state, much like moving a mouse cursor over a screen does not * perform any intrinsic actions in most applications. - * - * @param event - * - the motion event associated with the trackball. - * + * @param event - the motion event associated with the trackball. * @return This function returns true if the trackball motion was - * processed, which notifies the caller that this method - * handled the motion event and no other handling is - * necessary. + * processed, which notifies the caller that this method handled the + * motion event and no other handling is necessary. */ boolean doTrackballEvent(MotionEvent event) { synchronized (mSurfaceHolder) { - if (mMode == STATE_RUNNING) { + if (mMode == stateEnum.RUNNING) { if (event.getAction() == MotionEvent.ACTION_MOVE) { mTrackballDX += event.getX() * TRACKBALL_COEFFICIENT; return true; @@ -763,30 +744,26 @@ private double yFromScr(float y) { return (y - mDisplayDY) / mDisplayScale; } - // - // doTouchEvent() - Implement this method to handle touch screen - // motion events. - // - // This method will be called three times in succession for each - // touch, to process ACTION_DOWN, ACTION_UP, and ACTION_MOVE. - // - // Parameters - // event - The motion event. - // - // Returns - // True if the event was handled, false otherwise. - // - // + /** + * This method to handles touch screen motion events. + *

This method will be called three times in succession for each + * touch, to process ACTION_DOWN, + * ACTION_UP, and ACTION_MOVE. + * @param event - the motion event. + * @return true if the event was handled. + */ boolean doTouchEvent(MotionEvent event) { synchronized (mSurfaceHolder) { if(updateStateOnEvent(event)) return true; - if (mMode == STATE_RUNNING) { + if (mMode == stateEnum.RUNNING) { double x = xFromScr(event.getX()); double y = yFromScr(event.getY()); - // Set the values used when Point To Shoot is on. + /* + * Set the values used when Point To Shoot is on. + */ if (event.getAction() == MotionEvent.ACTION_DOWN) { if (y < TOUCH_FIRE_Y_THRESHOLD) { mTouchFire = true; @@ -797,7 +774,9 @@ else if (Math.abs(x - 318) <= TOUCH_SWAP_X_THRESHOLD) mTouchSwap = true; } - // Set the values used when Aim Then Shoot is on. + /* + * Set the values used when Aim Then Shoot is on. + */ if (event.getAction() == MotionEvent.ACTION_DOWN) { if (y < ATS_TOUCH_FIRE_Y_THRESHOLD) { mATSTouchFire = true; @@ -822,18 +801,14 @@ else if (event.getAction() == MotionEvent.ACTION_MOVE) { * processed, this function will return true, otherwise if the * calling method should also process the motion event, this * function will return false. - * - * @param event - * - The MotionEvent to process for the purpose of updating - * the game state. If this parameter is null, then the - * game state is forced to update if applicable based on - * the current game state. - * - * @return This function returns true to inform the calling - * - Function that the game state has been updated and that - * no further processing is necessary, and false to - * indicate that the caller should continue processing the - * motion event. + * @param event - The MotionEvent to process for the purpose of + * updating the game state. If this parameter is null, then the + * game state is forced to update if applicable based on the current + * game state. + * @return This function returns true to inform the + * calling function that the game state has been updated and that no + * further processing is necessary, and false to + * indicate that the caller should continue processing the event. */ private boolean updateStateOnEvent(MotionEvent event) { boolean event_action_down = false; @@ -845,28 +820,28 @@ else if (event.getAction() == MotionEvent.ACTION_DOWN) if (event_action_down) { switch (mMode) { - case STATE_ABOUT: + case ABOUT: if (!mBlankScreen) { - setState(STATE_RUNNING); + setState(stateEnum.RUNNING); return true; } break; - case STATE_PAUSE: + case PAUSED: if (mShowScores) { mShowScores = false; nextLevel(); if (getCurrentLevelIndex() != 0) - setState(STATE_RUNNING); + setState(stateEnum.RUNNING); if (mGameListener != null) { - mGameListener.onGameEvent(EVENT_LEVEL_START); + mGameListener.onGameEvent(eventEnum.LEVEL_START); } return true; } - setState(STATE_RUNNING); + setState(stateEnum.RUNNING); break; - case STATE_RUNNING: + case RUNNING: default: break; } @@ -962,15 +937,11 @@ private void drawAboutScreen(Canvas canvas) { /** * Draw the high score screen for puzzle game mode. - *

- * The objective of puzzle game mode is efficiency - fire as few + *

The objective of puzzle game mode is efficiency - fire as few * bubbles as possible as quickly as possible. Thus the high score * will exhibit the fewest shots fired the quickest. - * - * @param canvas - * - the drawing canvas to display the scores on. - * @param level - * - the level index. + * @param canvas - the drawing canvas to display the scores on. + * @param level - the level index. */ private void drawHighScoreScreen(Canvas canvas, int level) { canvas.drawRGB(0, 0, 0); @@ -1029,16 +1000,17 @@ private void updateGameState() { if ((mFrozenGame == null) || (mHighscoreManager == null)) return; - int game_state = mFrozenGame.play(mLeft || mWasLeft, - mRight || mWasRight, - mFire || mUp || mWasFire || mWasUp, - mDown || mWasDown || mTouchSwap, - mTrackballDX, - mTouchFire, mTouchX, mTouchY, - mATSTouchFire, mATSTouchDX); - if ((game_state == FrozenGame.GAME_NEXT_LOST) || - (game_state == FrozenGame.GAME_NEXT_WON )) { - if (game_state == FrozenGame.GAME_NEXT_WON) { + gameEnum gameState = mFrozenGame.play(mLeft || mWasLeft, + mRight || mWasRight, + mFire || mUp || mWasFire || mWasUp, + mDown || mWasDown || mTouchSwap, + mTrackballDX, + mTouchFire, mTouchX, mTouchY, + mATSTouchFire, mATSTouchDX); + + if ((gameState == gameEnum.NEXT_LOST) || + (gameState == gameEnum.NEXT_WON )) { + if (gameState == gameEnum.NEXT_WON) { mShowScores = true; pause(); if (FrozenBubble.getAdsOn() && @@ -1048,14 +1020,17 @@ private void updateGameState() { mContext.startActivity(intent); } } - else + else { nextLevel(); + } if (mGameListener != null) { - if (game_state == FrozenGame.GAME_NEXT_WON) - mGameListener.onGameEvent(EVENT_GAME_WON); - else - mGameListener.onGameEvent(EVENT_GAME_LOST); + if (gameState == gameEnum.NEXT_WON) { + mGameListener.onGameEvent(eventEnum.GAME_WON); + } + else { + mGameListener.onGameEvent(eventEnum.GAME_LOST); + } } } mWasLeft = false; @@ -1083,12 +1058,14 @@ private void nextLevel() { public void cleanUp() { synchronized (mSurfaceHolder) { - // I don't really understand why all this is necessary. - // I used to get a crash (an out-of-memory error) once every six or - // seven times I started the game. I googled the error and someone - // said you have to call recycle() on all the bitmaps and set - // the pointers to null to facilitate garbage collection. So I did - // and the crashes went away. + /* + * I don't really understand why all this is necessary. + * I used to get a crash (an out-of-memory error) once every six + * or seven times I started the game. I googled the error and + * someone said you have to call recycle() on all the bitmaps + * and set the pointers to null to facilitate garbage + * collection. So I did and the crashes went away. + */ mFrozenGame = null; mImagesReady = false; @@ -1314,14 +1291,13 @@ public void surfaceDestroyed(SurfaceHolder holder) { public void cleanUp() { //Log.i("frozen-bubble", "GameView.cleanUp()"); mGameThread.cleanUp(); - mContext = null; } /** * This is a class that extends TimerTask to resume displaying the * game screen as normal after it has been shown as a blank screen. - * * @author Eric Fortin + * */ class resumeGameScreenTask extends TimerTask { @Override @@ -1334,19 +1310,16 @@ public void run() { /** * Display a blank screen (black background) for the specified wait * interval. - * - * @param clearScreen - * - If true, show a blank screen for the specified wait - * interval. If false, show the normal screen. - * - * @param wait - * - The amount of time to display the blank screen. + * @param clearScreen - If true, show a blank screen for + * the specified wait interval. If false, show the + * normal screen. + * @param wait - The amount of time to display the blank screen. */ public void clearGameScreen(boolean clearScreen, int wait) { mBlankScreen = clearScreen; try { if (clearScreen) { - mGameThread.setState(GameThread.STATE_ABOUT); + mGameThread.setState(stateEnum.ABOUT); Timer timer = new Timer(); timer.schedule(new resumeGameScreenTask(), wait, wait + 1); } diff --git a/src/org/jfedor/frozenbubble/LevelManager.java b/src/org/jfedor/frozenbubble/LevelManager.java index fae29bb..c87ad1a 100644 --- a/src/org/jfedor/frozenbubble/LevelManager.java +++ b/src/org/jfedor/frozenbubble/LevelManager.java @@ -91,15 +91,11 @@ public void restoreState(Bundle map) { /** * Constructor used to provide randomly generated levels. - * - * @param seed - * - the random bubble generation seed. - * - * @param difficulty - * - the number of different bubble colors to generate. Higher - * numbers make the level more difficult to play. Use the - * static difficulty values defined in this class to set the - * level difficulty (e.g., EASY, HARD, etc.). + * @param seed - the random bubble generation seed. + * @param difficulty - the number of different bubble colors to + * generate. Higher numbers make the level more difficult to play. + * Use the static difficulty values defined in this class to set the + * level difficulty, e.g. EASY, HARD, etc. */ public LevelManager(long seed, int difficulty) { this.randomMode = true; @@ -116,12 +112,8 @@ public LevelManager(long seed, int difficulty) { /** * Constructor used to parse levels provided via a formatted array. - * - * @param levels - * - the byte array containing the level information. - * - * @param startingLevel - * - the current level starting index. + * @param levels - the byte array containing the level information. + * @param startingLevel - the current level starting index. */ public LevelManager(byte[] levels, int startingLevel) { randomMode = false; diff --git a/src/org/jfedor/frozenbubble/MultiplayerGameView.java b/src/org/jfedor/frozenbubble/MultiplayerGameView.java index 5bb8827..f5bf55b 100644 --- a/src/org/jfedor/frozenbubble/MultiplayerGameView.java +++ b/src/org/jfedor/frozenbubble/MultiplayerGameView.java @@ -79,6 +79,11 @@ import java.util.Vector; import org.gsanson.frozenbubble.MalusBar; +import org.jfedor.frozenbubble.GameScreen.eventEnum; +import org.jfedor.frozenbubble.GameScreen.gameEnum; +import org.jfedor.frozenbubble.GameScreen.stateEnum; +import org.jfedor.frozenbubble.MultiplayerGameView.NetGameInterface.NetworkStatus; +import org.jfedor.frozenbubble.MultiplayerGameView.NetGameInterface.RemoteInterface; import android.app.Activity; import android.content.Context; @@ -89,6 +94,7 @@ import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.KeyEvent; import android.view.MotionEvent; @@ -99,107 +105,642 @@ import com.efortin.frozenbubble.ComputerAI; import com.efortin.frozenbubble.HighscoreDO; import com.efortin.frozenbubble.HighscoreManager; +import com.efortin.frozenbubble.NetworkGameManager; +import com.efortin.frozenbubble.NetworkGameManager.GameFieldData; +import com.efortin.frozenbubble.NetworkGameManager.PlayerAction; +import com.efortin.frozenbubble.NetworkGameManager.connectEnum; +import com.efortin.frozenbubble.VirtualInput; -class MultiplayerGameView extends SurfaceView implements SurfaceHolder.Callback { +public class MultiplayerGameView extends SurfaceView + implements SurfaceHolder.Callback { - public static final int GAMEFIELD_WIDTH = 320; - public static final int GAMEFIELD_HEIGHT = 480; - public static final int EXTENDED_GAMEFIELD_WIDTH = 640; + public static final int GAMEFIELD_WIDTH = 320; + public static final int GAMEFIELD_HEIGHT = 480; + public static final int EXTENDED_GAMEFIELD_WIDTH = 640; + + /* + * The following screen orientation definitions were added to + * ActivityInfo in API level 9. + */ + public final static int SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 6; + public final static int SCREEN_ORIENTATION_SENSOR_PORTRAIT = 7; + public final static int SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8; + public final static int SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9; private int numPlayer1GamesWon; private int numPlayer2GamesWon; private Context mContext; private MultiplayerGameThread mGameThread; + private NetworkGameManager mNetworkManager; + private RemoteInterface remoteInterface; private ComputerAI mOpponent; - //********************************************************** + private VirtualInput mLocalInput; + private VirtualInput mRemoteInput; + private PlayerInput mPlayer1; + private PlayerInput mPlayer2; + private boolean muteKeyToggle = false; + private boolean pauseKeyToggle = false; + + //******************************************************************** // Listener interface for various events - //********************************************************** - // Event types. - public static final int EVENT_GAME_WON = 2; - public static final int EVENT_GAME_LOST = 3; - public static final int EVENT_GAME_PAUSED = 4; - public static final int EVENT_GAME_RESUME = 5; - public static final int EVENT_LEVEL_START = 6; - - // Listener user set. + //******************************************************************** + + /** + * Game event listener user set. + * @author Eric Fortin + * + */ public interface GameListener { - public abstract void onGameEvent(int event); + public abstract void onGameEvent(eventEnum event); } GameListener mGameListener; - public void setGameListener (GameListener gl) { + public void setGameListener(GameListener gl) { mGameListener = gl; } - // - // The following screen orientation definitions were added to - // ActivityInfo in API level 9. - // - // - public final static int SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 6; - public final static int SCREEN_ORIENTATION_SENSOR_PORTRAIT = 7; - public final static int SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8; - public final static int SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9; + /** + * Network game interface. This interface declares methods that must + * be implemented by the network management class to implement a + * distributed network multiplayer game. + * @author Eric Fortin + * + */ + public interface NetGameInterface { + /** + * This class encapsulates player action and game field storage for + * use by the game thread to determine when to process remote player + * actions and game field bubble grid synchronization tasks. + * @author Eric Fortin + * + */ + public class RemoteInterface { + public boolean gotAction; + public boolean gotFieldData; + public PlayerAction playerAction; + public GameFieldData gameFieldData; + + public RemoteInterface(PlayerAction action, GameFieldData fieldData) { + gotAction = false; + gotFieldData = false; + playerAction = action; + gameFieldData = fieldData; + } - /* - * TODO: implement keyboard keypress functionality. + public void cleanUp() { + gotAction = false; + gotFieldData = false; + playerAction = null; + gameFieldData = null; + } + }; + + /** + * A class to encapsulate all network status variables used in + * drawing the network status screen. + * @author efortin + * + */ + public class NetworkStatus { + public int localPlayerId; + public int remotePlayerId; + public boolean isConnected; + public boolean reservedGameId; + public boolean playerJoined; + public boolean gotFieldData; + public boolean gotPrefsData; + public boolean readyToPlay; + public String localIpAddress; + public String remoteIpAddress; + + public NetworkStatus() { + isConnected = false; + reservedGameId = false; + playerJoined = false; + gotFieldData = false; + gotPrefsData = false; + readyToPlay = false; + localIpAddress = null; + remoteIpAddress = null; + } + }; + + /* + * Force the implementer to supply the following methods. + */ + public abstract void checkRemoteChecksum(); + public abstract void cleanUp(); + public abstract boolean gameIsReadyForAction(); + public abstract short getLatestRemoteActionId(); + public abstract boolean getRemoteAction(); + public abstract PlayerAction getRemoteActionPreview(); + public abstract RemoteInterface getRemoteInterface(); + public abstract void newGame(); + public abstract void pause(); + public abstract void sendLocalPlayerAction(int playerId, + boolean compress, + boolean launch, + boolean swap, + int keyCode, + int launchColor, + int nextColor, + int newNextColor, + int attackBarBubbles, + byte attackBubbles[], + double aimPosition); + public abstract void setLocalChecksum(short checksum); + public abstract void setRemoteChecksum(short checksum); + public abstract void unPause(); + public abstract void updateNetworkStatus(NetworkStatus status); + } + + /** + * This class encapsulates player input action variables and methods. + *

This is to provide a common interface to the game independent + * of the input source. + * @author Eric Fortin + * */ - // Change mode (normal/colorblind) - public final static int KEY_M = 77; - // Pause/resume game - public final static int KEY_P = 80; - // Toggle sound on/off - public final static int KEY_S = 83; - boolean modeKeyPressed, pauseKeyPressed, soundKeyPressed; + class PlayerInput extends VirtualInput { + private boolean mCenter = false; + private boolean mDown = false; + private boolean mLeft = false; + private boolean mRight = false; + private boolean mUp = false; + private double mTrackballDx = 0; + private boolean mTouchSwap = false; + private double mTouchX; + private double mTouchY; + private boolean mTouchFireATS = false; + private double mTouchDxATS = 0; + private double mTouchLastX = 0; + + /** + * Construct and configure this player input instance. + * @param id - the player ID, e.g., + * VirtualInput.PLAYER1. + * @param type - true if the player is a simulation. + * @param remote - true if this player is playing on a + * remote machine, false if this player is local. + * @see VirtualInput + */ + public PlayerInput(int id, boolean type, boolean remote) { + init(); + configure(id, type, remote); + } + + /** + * Check if a center button press action is active. + * @return True if the player pressed the center button. + */ + public boolean actionCenter() { + boolean tempCenter = mWasCenter; + mWasCenter = false; + return tempCenter; + } + + /** + * Check if a bubble launch action is active. + * @return True if the player is launching a bubble. + */ + public boolean actionUp() { + boolean tempFire = mWasCenter || mWasUp; + mWasCenter = false; + mWasUp = false; + return mCenter || mUp || tempFire; + } + + /** + * Check if a move left action is active. + * @return True if the player is moving left. + */ + public boolean actionLeft() { + boolean tempLeft = mWasLeft; + mWasLeft = false; + return mLeft || tempLeft; + } + + /** + * Check if a move right action is active. + * @return True if the player is moving right. + */ + public boolean actionRight() { + boolean tempRight = mWasRight; + mWasRight = false; + return mRight || tempRight; + } + + /** + * Check if a bubble swap action is active. + * @return True if the player is swapping the launch bubble. + */ + public boolean actionDown() { + boolean tempSwap = mWasDown || mTouchSwap; + mWasDown = false; + mTouchSwap = false; + return mDown || tempSwap; + } + + /** + * Check if a touchscreen initiated bubble launch is active. + * @return True if the player is launching a bubble. + */ + public boolean actionTouchFire() { + boolean tempFire = mTouchFire; + mTouchFire = false; + return tempFire; + } + + /** + * Check if an ATS (aim-then-shoot) touchscreen initiated bubble + * launch is active. + * @return True if the player is launching a bubble. + */ + public boolean actionTouchFireATS() { + boolean tempFire = mTouchFireATS; + mTouchFireATS = false; + return tempFire; + } + + /** + * Based on the provided keypress, check if it corresponds to a new + * player action. + * @param keyCode + * @return True if the current keypress indicates a new player action. + */ + public boolean checkNewActionKeyPress(int keyCode) { + return (!mLeft && !mRight && !mCenter && !mUp && !mDown) && + ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT) || + (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) || + (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) || + (keyCode == KeyEvent.KEYCODE_DPAD_UP) || + (keyCode == KeyEvent.KEYCODE_DPAD_DOWN)); + } + + /** + * Obtain the ATS (aim-then-shoot) touch horizontal position change. + * @return The horizontal touch change in position. + */ + public double getTouchDxATS() { + double tempDx = mTouchDxATS; + mTouchDxATS = 0; + return tempDx; + } + + /** + * Obtain the horizontal touch position. + * @return The horizontal touch position. + */ + public double getTouchX() { + return mTouchX; + } + + /** + * Obtain the vertical touch position. + * @return The vertical touch position. + */ + public double getTouchY() { + return mTouchY; + } + + /** + * Obtain the trackball position change. + * @return The trackball position change. + */ + public double getTrackBallDx() { + double tempDx = mTrackballDx; + mTrackballDx = 0; + return tempDx; + } + + public void init() { + this.init_vars(); + mTrackballDx = 0; + mTouchFire = false; + mTouchSwap = false; + mTouchFireATS = false; + mTouchDxATS = 0; + } + + /** + * Process key presses. + * @param keyCode + * @return True if the key press was processed, false if not. + */ + public boolean setKeyDown(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mLeft = true; + mWasLeft = true; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mRight = true; + mWasRight = true; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + mCenter = true; + mWasCenter = true; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + mUp = true; + mWasUp = true; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mDown = true; + mWasDown = true; + return true; + } + return false; + } + + /** + * Process key releases. + * @param keyCode + * @return True if the key release was processed, false if not. + */ + public boolean setKeyUp(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mLeft = false; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mRight = false; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + mCenter = false; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + mUp = false; + return true; + } + else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mDown = false; + return true; + } + return false; + } + + public boolean setTouchEvent(int event, double x, double y) { + if (mGameThread.mMode == stateEnum.RUNNING) { + // Set the values used when Point To Shoot is on. + if (event == MotionEvent.ACTION_DOWN) { + if (y < MultiplayerGameThread.TOUCH_FIRE_Y_THRESHOLD) { + mTouchFire = true; + mTouchX = x; + mTouchY = y; + } + else if (Math.abs(x - 318) <= + MultiplayerGameThread.TOUCH_SWAP_X_THRESHOLD) + mTouchSwap = true; + } + + // Set the values used when Aim Then Shoot is on. + if (event == MotionEvent.ACTION_DOWN) { + if (y < MultiplayerGameThread.ATS_TOUCH_FIRE_Y_THRESHOLD) { + mTouchFireATS = true; + } + mTouchLastX = x; + } + else if (event == MotionEvent.ACTION_MOVE) { + if (y >= MultiplayerGameThread.ATS_TOUCH_FIRE_Y_THRESHOLD) { + mTouchDxATS = (x - mTouchLastX) * + MultiplayerGameThread.ATS_TOUCH_COEFFICIENT; + } + mTouchLastX = x; + } + return true; + } + return false; + } + + /** + * Accumulate the change in trackball horizontal position. + * @param trackBallDX + */ + public void setTrackBallDx(double trackBallDX) { + mTrackballDx += trackBallDX; + } + } + + private boolean checkImmediateAction() { + boolean actNow = false; + /* + * Preview the current action if one is available to see if it + * contains an asynchronous action (e.g., launch bubble swap). + */ + PlayerAction previewAction = mNetworkManager.getRemoteActionPreview(); + + if (previewAction != null) { + actNow = previewAction.compress || previewAction.swapBubble; + } + + return actNow; + } + + private void monitorRemotePlayer() { + if ((mNetworkManager != null) && (mRemoteInput != null)) { + /* + * Check the remote player interface for game field updates. + * Reject the game field data if it doesn't correspond to the + * latest remote player game field. This is determined based on + * whether the game field data action ID matches the latest remote + * player action ID. + */ + if (remoteInterface.gotFieldData) { + if (mNetworkManager.getLatestRemoteActionId() == + remoteInterface.gameFieldData.localActionID) { + setPlayerGameField(remoteInterface.gameFieldData); + } + remoteInterface.gotFieldData = false; + } + + /* + * Once the game is ready, if the game thread is not running, then + * allow the remote player to update the game thread state. + * + * If an asynchronous action is available or we are clear to + * perform a synchronous action, retrieve and clear the current + * available action from the action queue. + */ + if (mNetworkManager.gameIsReadyForAction() && (checkImmediateAction() || + mRemoteInput.mGameRef.getOkToFire() || + (mGameThread.mMode != stateEnum.RUNNING))) { + if (mNetworkManager.getRemoteAction()) { + setPlayerAction(remoteInterface.playerAction); + remoteInterface.gotAction = false; + } + else if (mRemoteInput.mGameRef.getOkToFire()) { + mNetworkManager.checkRemoteChecksum(); + } + } + } + } + + /** + * Set the player action for a remote player - as in a person playing + * via a client device over a network. + * @param newAction - the object containing the remote input info. + */ + private synchronized void setPlayerAction(PlayerAction newAction) { + if (newAction == null) { + return; + } + + VirtualInput playerRef; + + if (newAction.playerID == VirtualInput.PLAYER1) { + playerRef = mPlayer1; + } + else if (newAction.playerID == VirtualInput.PLAYER2) { + playerRef = mPlayer2; + } + else { + return; + } + + if (mGameThread != null) + mGameThread.updateStateOnEvent(null); + + /* + * Set the launcher bubble colors. + */ + if ((newAction.launchBubbleColor > -1) && + (newAction.launchBubbleColor < 8) && + (newAction.nextBubbleColor > -1) && + (newAction.nextBubbleColor < 8) && + (newAction.newNextBubbleColor > -1) && + (newAction.newNextBubbleColor < 8)) { + playerRef.mGameRef.setLaunchBubbleColors(newAction.launchBubbleColor, + newAction.nextBubbleColor, + newAction.newNextBubbleColor); + } + + /* + * Set the launcher aim position. + */ + playerRef.mGameRef.setPosition(newAction.aimPosition); + + /* + * Process a compressor lower request. + */ + if (newAction.compress) { + playerRef.mGameRef.lowerCompressor(true); + } + + /* + * Process a bubble launch request. + */ + if (newAction.launchBubble) { + playerRef.setAction(KeyEvent.KEYCODE_DPAD_UP, true); + } + + /* + * Process a bubble swap request. + */ + if (newAction.swapBubble) { + playerRef.setAction(KeyEvent.KEYCODE_DPAD_DOWN, false); + } + + /* + * Process a pause/play button toggle request. + */ + if (newAction.keyCode == (byte) KeyEvent.KEYCODE_P) { + if (mGameThread != null) { + mGameThread.toggleKeyPress(KeyEvent.KEYCODE_P, true, false); + } + } + + /* + * Set the current value of the attack bar. + */ + if (newAction.attackBarBubbles > -1) { + playerRef.mGameRef.malusBar.setAttackBubbles(newAction.attackBarBubbles, + newAction.attackBubbles); + } + } + + /** + * Set the game field for a remote player - as in a person playing + * via a client device over a network. + * @param newGameField - the object containing the remote field data. + */ + private void setPlayerGameField(GameFieldData newField) { + if (newField == null) { + return; + } + + VirtualInput playerRef; + + if (newField.playerID == VirtualInput.PLAYER1) { + playerRef = mPlayer1; + } + else if (newField.playerID == VirtualInput.PLAYER2) { + playerRef = mPlayer2; + } + else { + return; + } + + /* + * Set the bubble grid. Note this must be done before lowering the + * compressor, as bubbles will be created using a non-compressed + * game field! + */ + playerRef.mGameRef.setGrid(newField.gameField); + + /* + * Lower the compressor and bubbles in play to the required number + * of compressor steps. + */ + playerRef.mGameRef.setCompressorSteps(newField.compressorSteps); + + /* + * Set the launcher bubble colors. + */ + playerRef.mGameRef.setLaunchBubbleColors(newField.launchBubbleColor, + newField.nextBubbleColor, + playerRef.mGameRef.getNewNextColor()); + + /* + * Set the current value of the attack bar. + */ + playerRef.mGameRef.malusBar.setAttackBubbles(newField.attackBarBubbles, + null); + } class MultiplayerGameThread extends Thread { private static final int FRAME_DELAY = 40; - public static final int STATE_RUNNING = 1; - public static final int STATE_PAUSE = 2; - public static final int STATE_ABOUT = 4; - - private static final double TRACKBALL_COEFFICIENT = 5; - private static final double TOUCH_BUTTON_THRESHOLD = 16; - private static final double TOUCH_FIRE_Y_THRESHOLD = 380; - private static final double TOUCH_SWAP_X_THRESHOLD = 14; - private static final double ATS_TOUCH_COEFFICIENT = 0.2; - private static final double ATS_TOUCH_FIRE_Y_THRESHOLD = 350; - - private boolean mImagesReady = false; - private boolean mRun = false; - private boolean mShowScores = false; - private boolean mSurfaceOK = false; - private boolean mLeft = false; - private boolean mRight = false; - private boolean mUp = false; - private boolean mDown = false; - private boolean mFire = false; - private boolean mWasLeft = false; - private boolean mWasRight = false; - private boolean mWasFire = false; - private boolean mWasUp = false; - private boolean mWasDown = false; - private double mTrackballDX = 0; - private boolean mTouchFire = false; - private boolean mTouchSwap = false; - private double mTouchX; - private double mTouchY; - private boolean mATSTouchFire = false; - private double mATSTouchDX = 0; - private double mATSTouchLastX; + public static final double TRACKBALL_COEFFICIENT = 5; + public static final double TOUCH_BUTTON_THRESHOLD = 16; + public static final double TOUCH_FIRE_Y_THRESHOLD = 380; + public static final double TOUCH_SWAP_X_THRESHOLD = 14; + public static final double ATS_TOUCH_COEFFICIENT = 0.2; + public static final double ATS_TOUCH_FIRE_Y_THRESHOLD = 350; + + private boolean mImagesReady = false; + private boolean mRun = false; + private boolean mShowNetwork = false; + private boolean mShowScores = false; + private boolean mSurfaceOK = false; private int mDisplayDX; private int mDisplayDY; private double mDisplayScale; private long mLastTime; - private int mMode; - private int mModeWas; private int mPlayer1DX; private int mPlayer2DX; + private stateEnum mMode; + private stateEnum mModeWas; + private Bitmap mBackgroundOrig; private Bitmap[] mBubblesOrig; private Bitmap[] mBubblesBlindOrig; @@ -251,7 +792,7 @@ class MultiplayerGameThread extends Thread { private SoundManager mSoundManager; private SurfaceHolder mSurfaceHolder; - private final HighscoreManager mHighscoreManager; + private final HighscoreManager mHighScoreManager; Vector mImageList; @@ -417,7 +958,10 @@ public void cleanUp() { mSoundManager.cleanUp(); mSoundManager = null; mLevelManager = null; - mHighscoreManager.close(); + + if (mHighScoreManager != null) { + mHighScoreManager.close(); + } } } @@ -440,16 +984,9 @@ private void doDraw(Canvas canvas) { /** * Process key presses. This must be allowed to run regardless of * the game state to correctly handle initial game conditions. - * - * @param keyCode - * - the static KeyEvent key identifier. - * - * @param msg - * - the key action message. - * - * @return - * - true if the key action is processed, false if not. - * + * @param keyCode - the static KeyEvent key identifier. + * @param msg - the key action message. + * @return - true if the key action is processed. * @see android.view.View#onKeyDown(int, android.view.KeyEvent) */ boolean doKeyDown(int keyCode, KeyEvent msg) { @@ -457,167 +994,105 @@ boolean doKeyDown(int keyCode, KeyEvent msg) { /* * Only update the game state if this is a fresh key press. */ - if ((!mLeft && !mRight && !mFire && !mUp && !mDown) && - ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT) || - (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) || - (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) || - (keyCode == KeyEvent.KEYCODE_DPAD_UP) || - (keyCode == KeyEvent.KEYCODE_DPAD_DOWN))) + if (mLocalInput.checkNewActionKeyPress(keyCode)) updateStateOnEvent(null); - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { - mLeft = true; - mWasLeft = true; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - mRight = true; - mWasRight = true; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { - mFire = true; - mWasFire = true; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { - mUp = true; - mWasUp = true; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - mDown = true; - mWasDown = true; - return true; - } - return false; + /* + * Process the key press if it is a function key. + */ + toggleKeyPress(keyCode, true, true); + + /* + * Process the key press if it is a game input key. + */ + return mLocalInput.setKeyDown(keyCode); } } /** * Process key releases. This must be allowed to run regardless of * the game state in order to properly clear key presses. - * - * @param keyCode - * - the static KeyEvent key identifier. - * - * @param msg - * - the key action message. - * - * @return true if the key action is processed, false if not. - * + * @param keyCode - the static KeyEvent key identifier. + * @param msg - the key action message. + * @return - true if the key action is processed. * @see android.view.View#onKeyUp(int, android.view.KeyEvent) */ boolean doKeyUp(int keyCode, KeyEvent msg) { synchronized (mSurfaceHolder) { - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { - mLeft = false; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - mRight = false; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { - mFire = false; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { - mUp = false; - return true; - } - else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - mDown = false; - return true; - } - return false; + return mLocalInput.setKeyUp(keyCode); } } /** * This method handles screen touch motion events. - *

- * This method will be called three times in succession for each - * touch, to process ACTION_DOWN, ACTION_UP, and ACTION_MOVE. - * - * @param event - * - the motion event - * @return True if the event was handled, false otherwise. + *

This method will be called three times in succession for each + * touch, to process ACTION_DOWN, + * ACTION_UP, and ACTION_MOVE. + * @param event - the motion event + * @return true if the event was handled.. */ boolean doTouchEvent(MotionEvent event) { synchronized (mSurfaceHolder) { + double x_offset; double x = xFromScr(event.getX()); double y = yFromScr(event.getY()); + if (mLocalInput.playerID == VirtualInput.PLAYER1) + x_offset = 0; + else + x_offset = -318; + /* + * Check for a pause button sprite press. This will toggle the + * pause button sprite between pause and play. If the game was + * previously paused by the pause button, ignore screen touches + * that aren't on the pause button sprite. + */ if (event.getAction() == MotionEvent.ACTION_DOWN) { if ((Math.abs(x - 183) <= TOUCH_BUTTON_THRESHOLD) && (Math.abs(y - 460) <= TOUCH_BUTTON_THRESHOLD)) { - pauseKeyPressed = !pauseKeyPressed; - if (mFrozenGame1 != null) - mFrozenGame1.pauseButtonPressed(pauseKeyPressed); + toggleKeyPress(KeyEvent.KEYCODE_P, false, true); } - else if (pauseKeyPressed) + else if (toggleKeyState(KeyEvent.KEYCODE_P)) return false; } + /* + * Update the game state (paused, running, etc.) if necessary. + */ if(updateStateOnEvent(event)) return true; - if ((mMode == STATE_RUNNING) && (pauseKeyPressed)) + /* + * If the game is running and the pause button sprite was pressed, + * pause the game. + */ + if ((mMode == stateEnum.RUNNING) && + (toggleKeyState(KeyEvent.KEYCODE_P))) pause(); - if (mMode == STATE_RUNNING) { - // Set the values used when Point To Shoot is on. - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (y < TOUCH_FIRE_Y_THRESHOLD) { - mTouchFire = true; - mTouchX = x; - mTouchY = y; - } - else if (Math.abs(x - 318) <= TOUCH_SWAP_X_THRESHOLD) - mTouchSwap = true; - } - - // Set the values used when Aim Then Shoot is on. - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (y < ATS_TOUCH_FIRE_Y_THRESHOLD) { - mATSTouchFire = true; - } - mATSTouchLastX = x; - } - else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (y >= ATS_TOUCH_FIRE_Y_THRESHOLD) { - mATSTouchDX = (x - mATSTouchLastX) * ATS_TOUCH_COEFFICIENT; - } - mATSTouchLastX = x; - } - return true; - } - return false; + /* + * Process the screen touch event. + */ + return mLocalInput.setTouchEvent(event.getAction(), x + x_offset, y); } } /** * Process trackball motion events. - *

- * This method only processes trackball motion for the purpose of + *

This method only processes trackball motion for the purpose of * aiming the launcher. The trackball has no effect on the game * state, much like moving a mouse cursor over a screen does not * perform any intrinsic actions in most applications. - * - * @param event - * - the motion event associated with the trackball. - * - * @return This function returns true if the trackball motion was - * processed, which notifies the caller that this method - * handled the motion event and no other handling is - * necessary. + * @param event - the motion event associated with the trackball. + * @return This function returns true if the trackball + * motion was processed, which notifies the caller that this method + * handled the motion event and no other handling is necessary. */ boolean doTrackballEvent(MotionEvent event) { synchronized (mSurfaceHolder) { - if (mMode == STATE_RUNNING) { + if (mMode == stateEnum.RUNNING) { if (event.getAction() == MotionEvent.ACTION_MOVE) { - mTrackballDX += event.getX() * TRACKBALL_COEFFICIENT; + mLocalInput.setTrackBallDx(event.getX() * TRACKBALL_COEFFICIENT); return true; } } @@ -690,17 +1165,18 @@ private void drawBackground(Canvas c) { /** * Draw the high score screen for multiplayer game mode. - *

- * The objective of multiplayer game mode is endurance - fire as + *

The objective of multiplayer game mode is endurance - fire as * many bubbles as possible for as long as possible. Thus the high * score will exhibit the most shots fired during the longest game. - * - * @param canvas - * - the drawing canvas to display the scores on. - * @param level - * - the level difficulty index. + * @param canvas - the drawing canvas to display the scores on. + * @param level - the level difficulty index. */ private void drawHighScoreScreen(Canvas canvas, int level) { + if (mHighScoreManager == null) { + mShowScores = false; + return; + } + canvas.drawRGB(0, 0, 0); int x = 168; int y = 20; @@ -714,12 +1190,12 @@ else if (orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) x -= GAMEFIELD_WIDTH/2; mFont.print("highscore for " + - LevelManager.DifficultyStrings[mHighscoreManager.getLevel()], + LevelManager.DifficultyStrings[mHighScoreManager.getLevel()], x, y, canvas, mDisplayScale, mDisplayDX, mDisplayDY); y += 2 * ysp; - List hlist = mHighscoreManager.getLowScore(level, 15); - long lastScoreId = mHighscoreManager.getLastScoreId(); + List hlist = mHighScoreManager.getLowScore(level, 15); + long lastScoreId = mHighScoreManager.getLastScoreId(); int i = 1; for (HighscoreDO hdo : hlist) { String you = ""; @@ -745,6 +1221,110 @@ else if (orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) } } + private void drawNetworkScreen(Canvas canvas) { + if (mNetworkManager == null) { + mShowNetwork = false; + return; + } + + canvas.drawRGB(0, 0, 0); + int x = 168; + int y = 20; + int ysp = 26; + int orientation = getScreenOrientation(); + + if (orientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT) + x += GAMEFIELD_WIDTH/2; + else if (orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + x -= GAMEFIELD_WIDTH/2; + + NetworkStatus status = new NetworkStatus(); + mNetworkManager.updateNetworkStatus(status); + + if (status.isConnected) { + mFont.print("internet status: ]", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + } + else { + mFont.print("internet status: _", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + } + + mFont.print("my address: " + status.localIpAddress, x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + + mFont.print("connect to: " + status.remoteIpAddress, x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + + if (status.reservedGameId) { + mFont.print("checking for games...|", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + } + else { + mFont.print("checking for games...", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + return; + } + + mFont.print("open game slot found!", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + + if (status.playerJoined) { + mFont.print("waiting for player " + status.remotePlayerId + "...|", + x, y, canvas, mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + } + else { + mFont.print("waiting for player " + status.remotePlayerId + "...", + x, y, canvas, mDisplayScale, mDisplayDX, mDisplayDY); + return; + } + + if (status.localPlayerId == VirtualInput.PLAYER2) { + if (status.gotPrefsData || status.readyToPlay) { + mFont.print("getting preferences...|", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + } + else { + mFont.print("getting preferences...", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + return; + } + } + + if (status.gotFieldData || status.readyToPlay) { + mFont.print("getting data...|", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + } + else { + mFont.print("getting data...", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + return; + } + + if (status.readyToPlay) { + mFont.print("waiting for game start...|", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + y += ysp; + } + else { + mFont.print("waiting for game start...", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + return; + } + + mFont.print("tap to begin playing!", x, y, canvas, + mDisplayScale, mDisplayDX, mDisplayDY); + } + private void drawWinTotals(Canvas canvas) { int y = 433; int x = GAMEFIELD_WIDTH - 40; @@ -804,12 +1384,11 @@ public int getCurrentLevelIndex() { } private int getScreenOrientation() { - // - // The method getOrientation() was deprecated in API level 8. - // - // For API level 8 or greater, use getRotation(). - // - // + /* + * The method getOrientation() was deprecated in API level 8. + * + * For API level 8 or greater, use getRotation(). + */ int rotation = ((Activity) mContext).getWindowManager(). getDefaultDisplay().getOrientation(); DisplayMetrics dm = new DisplayMetrics(); @@ -817,23 +1396,21 @@ private int getScreenOrientation() { int width = dm.widthPixels; int height = dm.heightPixels; int orientation; - // - // The orientation determination is based on the natural orienation - // mode of the device, which can be either portrait, landscape, or - // square. - // - // After the natural orientation is determined, convert the device - // rotation into a fully qualified orientation. - // - // + /* + * The orientation determination is based on the natural orienation + * mode of the device, which can be either portrait, landscape, or + * square. + * + * After the natural orientation is determined, convert the device + * rotation into a fully qualified orientation. + */ if ((((rotation == Surface.ROTATION_0 ) || (rotation == Surface.ROTATION_180)) && (height > width)) || (((rotation == Surface.ROTATION_90 ) || (rotation == Surface.ROTATION_270)) && (width > height))) { - // - // Natural orientation is portrait. - // - // + /* + * Natural orientation is portrait. + */ switch(rotation) { case Surface.ROTATION_0: orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; @@ -853,10 +1430,9 @@ private int getScreenOrientation() { } } else { - // - // Natural orientation is landscape or square. - // - // + /* + * Natural orientation is landscape or square. + */ switch(rotation) { case Surface.ROTATION_0: orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; @@ -890,11 +1466,13 @@ public MultiplayerGameThread(SurfaceHolder surfaceHolder) { //Log.i("frozen-bubble", "GameThread()"); mSurfaceHolder = surfaceHolder; Resources res = mContext.getResources(); - setState(STATE_PAUSE); + setState(stateEnum.PAUSED); BitmapFactory.Options options = new BitmapFactory.Options(); - // The Options.inScaled field is only available starting at API 4. + /* + * The Options.inScaled field is only available starting at API 4. + */ try { Field f = options.getClass().getField("inScaled"); f.set(options, Boolean.FALSE); @@ -1039,10 +1617,20 @@ public MultiplayerGameThread(SurfaceHolder surfaceHolder) { mFont = new BubbleFont(mFontImage); mLauncher = res.getDrawable(R.drawable.launcher); mSoundManager = new SoundManager(mContext); - mHighscoreManager = new HighscoreManager(getContext(), - HighscoreManager. - MULTIPLAYER_DATABASE_NAME); - mLevelManager = new LevelManager(0, FrozenBubble.getDifficulty()); + + /* + * Only keep a high score database when the opponent is the CPU. + */ + if (mRemoteInput.isCPU) { + mHighScoreManager = new HighscoreManager(getContext(), + HighscoreManager. + MULTIPLAYER_DATABASE_NAME); + } + else { + mHighScoreManager = null; + } + + mLevelManager = new LevelManager(0, FrozenBubble.getDifficulty()); newGame(); } @@ -1060,7 +1648,9 @@ public void newGame() { mCompressorHead, mCompressor, malusBar2, mLauncher, mSoundManager, mLevelManager, - mHighscoreManager, 1); + mHighScoreManager, mNetworkManager, + mPlayer1); + mPlayer1.setGameRef(mFrozenGame1); mFrozenGame2 = new FrozenGame(mBackground, mBubbles, mBubblesBlind, mFrozenBubbles, mTargetedBubbles, mBubbleBlink, mGameWon, mGameLost, @@ -1068,28 +1658,42 @@ public void newGame() { null, null, mPenguins2, mCompressorHead, mCompressor, malusBar1, mLauncher, - mSoundManager, mLevelManager, null, 2); - mHighscoreManager.startLevel(mLevelManager.getLevelIndex()); + mSoundManager, mLevelManager, + null, mNetworkManager, + mPlayer2); + mPlayer2.setGameRef(mFrozenGame2); + if (mHighScoreManager != null) { + mHighScoreManager.startLevel(mLevelManager.getLevelIndex()); + } + if (mNetworkManager != null) { + mNetworkManager.newGame(); + mShowNetwork = true; + } } } public void pause() { synchronized (mSurfaceHolder) { - if (mMode == STATE_RUNNING) { - setState(STATE_PAUSE); + if (mMode == stateEnum.RUNNING) { + setState(stateEnum.PAUSED); if (mGameListener != null) - mGameListener.onGameEvent(EVENT_GAME_PAUSED); + mGameListener.onGameEvent(eventEnum.GAME_PAUSED); if (mFrozenGame1 != null) mFrozenGame1.pause(); if (mFrozenGame2 != null) mFrozenGame2.pause(); - if (mHighscoreManager != null) - mHighscoreManager.pauseLevel(); + if (mHighScoreManager != null) + mHighScoreManager.pauseLevel(); } } } + public void pauseButtonPressed(boolean pauseKeyPressed) { + if (mFrozenGame1 != null) + mFrozenGame1.pauseButtonPressed(pauseKeyPressed); + } + private void resizeBitmaps() { //Log.i("frozen-bubble", "resizeBitmaps()"); scaleFrom(mBackground, mBackgroundOrig); @@ -1125,31 +1729,33 @@ private void resizeBitmaps() { } /** - * Restores game state from the indicated Bundle. Typically called when - * the Activity is being restored after having been previously + * Restores game state from the indicated Bundle. Typically called + * when the Activity is being restored after having been previously * destroyed. - * - * @param savedState - * - Bundle containing the game state. + * @param savedState - Bundle containing the game state. */ public synchronized void restoreState(Bundle map) { synchronized (mSurfaceHolder) { - setState(STATE_PAUSE); + setState(stateEnum.PAUSED); numPlayer1GamesWon = map.getInt("numPlayer1GamesWon", 0); numPlayer2GamesWon = map.getInt("numPlayer2GamesWon", 0); mFrozenGame1 .restoreState(map, mImageList); mFrozenGame2 .restoreState(map, mImageList); mLevelManager .restoreState(map); - mHighscoreManager.restoreState(map); + if (mHighScoreManager != null) { + mHighScoreManager.restoreState(map); + } } } public void resumeGame() { synchronized (mSurfaceHolder) { - if (mMode == STATE_RUNNING) { + if (mMode == stateEnum.RUNNING) { mFrozenGame1 .resume(); mFrozenGame2 .resume(); - mHighscoreManager.resumeLevel(); + if (mHighScoreManager != null) { + mHighScoreManager.resumeLevel(); + } } } } @@ -1172,22 +1778,29 @@ public void run() { if (c != null) { synchronized (mSurfaceHolder) { if (mRun) { - if (mMode == STATE_ABOUT) { + monitorRemotePlayer(); + if (mMode == stateEnum.ABOUT) { drawAboutScreen(c); } - else if (mMode == STATE_PAUSE) { - if (mShowScores) - drawHighScoreScreen(c, mHighscoreManager.getLevel()); + else if (mMode == stateEnum.PAUSED) { + if (mNetworkManager != null) { + if (mShowNetwork) + drawNetworkScreen(c); + else + doDraw(c); + } + else if ((mHighScoreManager != null) && mShowScores) + drawHighScoreScreen(c, mHighScoreManager.getLevel()); else doDraw(c); } else { - if (mMode == STATE_RUNNING) { - if (mModeWas != STATE_RUNNING) { + if (mMode == stateEnum.RUNNING) { + if (mModeWas != stateEnum.RUNNING) { if (mGameListener != null) - mGameListener.onGameEvent(EVENT_GAME_RESUME); + mGameListener.onGameEvent(eventEnum.GAME_RESUME); - mModeWas = STATE_RUNNING; + mModeWas = stateEnum.RUNNING; resumeGame(); } updateGameState(); @@ -1199,9 +1812,11 @@ else if (mMode == STATE_PAUSE) { } } } finally { - // do this in a finally so that if an exception is thrown - // during the above, we don't leave the Surface in an - // inconsistent state + /* + * Do this in a finally so that if an exception is thrown + * during the above, we don't leave the Surface in an + * inconsistent state. + */ if (c != null) mSurfaceHolder.unlockCanvasAndPost(c); } @@ -1211,7 +1826,6 @@ else if (mMode == STATE_PAUSE) { /** * Dump game state to the provided Bundle. Typically called when the * Activity is being suspended. - * * @return Bundle with this view's state */ public Bundle saveState(Bundle map) { @@ -1223,7 +1837,9 @@ public Bundle saveState(Bundle map) { mFrozenGame1 .saveState(map); mFrozenGame2 .saveState(map); mLevelManager .saveState(map); - mHighscoreManager.saveState(map); + if (mHighScoreManager != null) { + mHighScoreManager.saveState(map); + } } } return map; @@ -1245,30 +1861,28 @@ private void scaleFrom(BmpWrap image, Bitmap bmp) { } public void setPosition(double value) { - mFrozenGame1.setPosition(value); + mLocalInput.mGameRef.setPosition(value); } public void setRunning(boolean b) { mRun = b; } - public void setState(int mode) { + public void setState(stateEnum newMode) { synchronized (mSurfaceHolder) { - // - // Only update the previous mode storage if the new mode is - // different from the current mode, in case the same mode is - // being set multiple times. - // - // The transition from state to state must be preserved in - // case a separate execution thread that checks for state - // transitions does not get a chance to run between calls to - // this method. - // - // - if (mode != mMode) + /* + * Only update the previous mode storage if the new mode is + * different from the current mode, in case the same mode is + * being set multiple times. + * + * The transition from state to state must be preserved in case + * a separate execution thread that checks for state transitions + * does not get a chance to run between calls to this method. + */ + if (newMode != mMode) mModeWas = mMode; - mMode = mode; + mMode = newMode; } } @@ -1293,17 +1907,26 @@ public void setSurfaceSize(int width, int height) { else { mDisplayScale = (1.0 * newWidth) / gameWidth; /* + * When rotate to shoot targeting mode is selected during a + * multiplayer game, then the screen orientation is forced to + * landscape. + * * In portrait mode during a multiplayer game, display just * one game field. Depending on which portrait mode it is, * display player one or player two. For normal portrait * orientation, show player one, and for reverse portrait, * show player two. */ - int orientation = getScreenOrientation(); - if (orientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT) - mDisplayDX = (int)(-mDisplayScale * gameWidth); - else + if (FrozenBubble.getTargetMode() == FrozenBubble.ROTATE_TO_SHOOT) { mDisplayDX = 0; + } + else { + int orientation = getScreenOrientation(); + if (orientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT) + mDisplayDX = (int)(-mDisplayScale * gameWidth); + else + mDisplayDX = 0; + } mDisplayDY = (int)((newHeight - (mDisplayScale * gameHeight)) / 2); } mPlayer1DX = (int) (mDisplayDX - (mDisplayScale * ( gameWidth / 2 ))); @@ -1312,13 +1935,21 @@ public void setSurfaceSize(int width, int height) { } } + /** + * Create a CPU opponent object (if necessary) and start the thread. + */ public void startOpponent() { if (mOpponent != null) { mOpponent.stopThread(); mOpponent = null; } - mOpponent = new ComputerAI(mFrozenGame2); - mOpponent.start(); + if (mRemoteInput.isCPU) { + if (mRemoteInput.playerID == VirtualInput.PLAYER2) + mOpponent = new ComputerAI(mFrozenGame2, mRemoteInput); + else + mOpponent = new ComputerAI(mFrozenGame1, mRemoteInput); + mOpponent.start(); + } } public boolean surfaceOK() { @@ -1327,23 +1958,68 @@ public boolean surfaceOK() { } } + /** + * Process function key presses. Function keys toggle features on + * and off (e.g., game paused on/off, sound on/off, etc.). + * @param keyCode - the key code to process. + * @param updateNow - if true, apply state changes. + * @param transmit - if true and this is a network game, send the + * key code over the network. + */ + public void toggleKeyPress(int keyCode, + boolean updateNow, + boolean transmit) { + if (keyCode == KeyEvent.KEYCODE_M) + muteKeyToggle = !muteKeyToggle; + else if (keyCode == KeyEvent.KEYCODE_P) { + if (transmit && (mNetworkManager != null)) { + mNetworkManager.sendLocalPlayerAction(mLocalInput.playerID, + false, false, false, keyCode, -1, -1, -1, -1, null, + mLocalInput.mGameRef.launchBubblePosition); + } + pauseKeyToggle = !pauseKeyToggle; + mGameThread.pauseButtonPressed(pauseKeyToggle); + if (updateNow) { + updateStateOnEvent(null); + /* + * If the game is running and the pause button was pressed, + * pause the game. + */ + if (pauseKeyToggle && (mMode == stateEnum.RUNNING)) + pause(); + } + } + } + + /** + * Obtain the current state of a feature toggle key. + * @param keyCode + * @return The state of the desired feature toggle key flag. + */ + public boolean toggleKeyState(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_M) + return muteKeyToggle; + else if (keyCode == KeyEvent.KEYCODE_P) + return pauseKeyToggle; + + return false; + } + /** * updateStateOnEvent() - a common method to process motion events * to set the game state. When the motion event has been fully * processed, this function will return true, otherwise if the * calling method should also process the motion event, this * function will return false. - * - * @param event - * - The MotionEvent to process for the purpose of updating - * the game state. If this parameter is null, then the - * game state is forced to update if applicable based on - * the current game state. - * - * @return This function returns true to inform the calling function - * that the game state has been updated and that no further - * processing is necessary, and false to indicate that the - * caller should continue processing the motion event. + * @param event - the MotionEvent to process for the purpose of + * updating the game state. If this parameter is null, then the + * game state is forced to update if applicable based on the current + * game state. + * @return This function returns true to inform the + * calling function that the game state has been updated and that no + * further processing is necessary, and false to + * indicate that the caller should continue processing the motion + * event. */ private boolean updateStateOnEvent(MotionEvent event) { boolean event_action_down = false; @@ -1355,23 +2031,35 @@ else if (event.getAction() == MotionEvent.ACTION_DOWN) if (event_action_down) { switch (mMode) { - case STATE_ABOUT: - setState(STATE_RUNNING); + case ABOUT: + setState(stateEnum.RUNNING); return true; - case STATE_PAUSE: - if (mShowScores) { + case PAUSED: + if (mNetworkManager != null) { + if (mShowNetwork) { + if (mNetworkManager.gameIsReadyForAction()) { + mShowNetwork = false; + setState(stateEnum.RUNNING); + if (mGameListener != null) { + mGameListener.onGameEvent(eventEnum.LEVEL_START); + } + } + return true; + } + } + else if (mShowScores) { mShowScores = false; - setState(STATE_RUNNING); + setState(stateEnum.RUNNING); if (mGameListener != null) { - mGameListener.onGameEvent(EVENT_LEVEL_START); + mGameListener.onGameEvent(eventEnum.LEVEL_START); } return true; } - setState(STATE_RUNNING); + setState(stateEnum.RUNNING); break; - case STATE_RUNNING: + case RUNNING: default: break; } @@ -1381,90 +2069,126 @@ else if (event.getAction() == MotionEvent.ACTION_DOWN) private void updateGameState() { if ((mFrozenGame1 == null) || (mFrozenGame2 == null) || - (mOpponent == null) || (mHighscoreManager == null)) + ((mOpponent == null) && mRemoteInput.isCPU)) { return; + } - int game1_state = mFrozenGame1.play(mLeft || mWasLeft, - mRight || mWasRight, - mFire || mUp || mWasFire || mWasUp, - mDown || mWasDown || mTouchSwap, - mTrackballDX, - mTouchFire, mTouchX, mTouchY, - mATSTouchFire, mATSTouchDX); - mFrozenGame2.play(mOpponent.getAction() == KeyEvent.KEYCODE_DPAD_LEFT, - mOpponent.getAction() == KeyEvent.KEYCODE_DPAD_RIGHT, - mOpponent.getAction() == KeyEvent.KEYCODE_DPAD_UP, - mOpponent.getAction() == KeyEvent.KEYCODE_DPAD_DOWN, - 0, false, 0, 0, false, 0); - mOpponent.clearAction(); + gameEnum game1State = mFrozenGame1.play(mPlayer1.actionLeft(), + mPlayer1.actionRight(), + mPlayer1.actionUp(), + mPlayer1.actionDown(), + mPlayer1.getTrackBallDx(), + mPlayer1.actionTouchFire(), + mPlayer1.getTouchX(), + mPlayer1.getTouchY(), + mPlayer1.actionTouchFireATS(), + mPlayer1.getTouchDxATS()); + + gameEnum game2State = mFrozenGame2.play(mPlayer2.actionLeft(), + mPlayer2.actionRight(), + mPlayer2.actionUp(), + mPlayer2.actionDown(), + mPlayer2.getTrackBallDx(), + mPlayer2.actionTouchFire(), + mPlayer2.getTouchX(), + mPlayer2.getTouchY(), + mPlayer2.actionTouchFireATS(), + mPlayer2.getTouchDxATS()); + /* + * If playing a network game, update the bubble grid checksums. + */ + if (mNetworkManager != null) { + mNetworkManager.setLocalChecksum(mLocalInput.mGameRef.gridChecksum); + mNetworkManager.setRemoteChecksum(mRemoteInput.mGameRef.gridChecksum); + } + + /* + * If playing a CPU opponent, notify the computer that the current + * action has been processed and we are ready for a new action. + */ + if (mOpponent != null) { + mOpponent.clearAction(); + } + + /* + * Obtain the number of attack bubbles to add to each player's + * attack bar that are being sent by their respective opponents. + */ malusBar1.addBubbles(mFrozenGame1.getSendToOpponent()); malusBar2.addBubbles(mFrozenGame2.getSendToOpponent()); - int game1_result = mFrozenGame1.getGameResult(); - int game2_result = mFrozenGame2.getGameResult(); + gameEnum game1Result = mFrozenGame1.getGameResult(); + gameEnum game2Result = mFrozenGame2.getGameResult(); /* - * When one player wins or loses, the other player is designated the an - * automatic loss or win, respectively. + * When one player wins or loses, the other player is + * automatically designated the loser or winner, respectively. */ - if (game1_result != FrozenGame.GAME_PLAYING) { - if ((game1_result == FrozenGame.GAME_WON) || - (game1_result == FrozenGame.GAME_NEXT_WON)) - mFrozenGame2.setGameResult(FrozenGame.GAME_LOST); - else - mFrozenGame2.setGameResult(FrozenGame.GAME_WON); + if (game1Result != gameEnum.PLAYING) { + if ((game1Result == gameEnum.WON) || + (game1Result == gameEnum.NEXT_WON)) { + mFrozenGame2.setGameResult(gameEnum.LOST); + } + else { + mFrozenGame2.setGameResult(gameEnum.WON); + } } - else if (game2_result != FrozenGame.GAME_PLAYING) { - if ((game2_result == FrozenGame.GAME_WON) || - (game2_result == FrozenGame.GAME_NEXT_WON)) { - mHighscoreManager.lostLevel(); - mFrozenGame1.setGameResult(FrozenGame.GAME_LOST); + else if (game2Result != gameEnum.PLAYING) { + if ((game2Result == gameEnum.WON) || + (game2Result == gameEnum.NEXT_WON)) { + if (mHighScoreManager != null) { + mHighScoreManager.lostLevel(); + } + mFrozenGame1.setGameResult(gameEnum.LOST); } else { - mHighscoreManager.endLevel(mFrozenGame1.nbBubbles); - mFrozenGame1.setGameResult(FrozenGame.GAME_WON); + if (mHighScoreManager != null) { + mHighScoreManager.endLevel(mFrozenGame1.nbBubbles); + } + mFrozenGame1.setGameResult(gameEnum.WON); } } /* - * Only start a new game when player 1 provides input, because the - * CPU is prone to sneaking a launch attempt in after the game is - * decided. + * Only start a new game when player 1 provides input when the + * opponent is the CPU, because the CPU is prone to sneaking a + * launch attempt in after the game is decided. + * + * Otherwise, the first player to provide input initiates the + * new game. */ - if ((game1_state == FrozenGame.GAME_NEXT_LOST) || - (game1_state == FrozenGame.GAME_NEXT_WON )) { - if (game1_state == FrozenGame.GAME_NEXT_WON ) + if (((game1State == gameEnum.NEXT_LOST) || + (game1State == gameEnum.NEXT_WON)) || + (!mRemoteInput.isCPU && + ((game2State == gameEnum.NEXT_LOST) || + (game2State == gameEnum.NEXT_WON)))){ + if ((game1State == gameEnum.NEXT_WON) || + (game2State == gameEnum.NEXT_LOST)) { numPlayer1GamesWon++; - else + } + else { numPlayer2GamesWon++; + } + + if (mNetworkManager == null) { + mShowScores = true; + } - mShowScores = true; pause(); newGame(); - startOpponent(); - } - mWasLeft = false; - mWasRight = false; - mWasFire = false; - mWasUp = false; - mWasDown = false; - mTrackballDX = 0; - mTouchFire = false; - mTouchSwap = false; - mATSTouchFire = false; - mATSTouchDX = 0; + if (mRemoteInput.isCPU) { + startOpponent(); + } + } } /** * Use the player 1 offset to calculate the horizontal offset to * apply a raw horizontal position to the playfield. - * - * @param x - * - the raw horizontal position. - * - * @return the adjusted horizontal position. + * @param x - the raw horizontal position. + * @return The adjusted horizontal position. */ private double xFromScr(float x) { return (x - mPlayer1DX) / mDisplayScale; @@ -1475,27 +2199,110 @@ private double yFromScr(float y) { } } - public MultiplayerGameView(Context context, int numPlayers) { + /** + * MultiplayerGameView class constructor. + * @param context - the application context. + * @param attrs - the compiled XML attributes for the superclass. + */ + public MultiplayerGameView(Context context, AttributeSet attrs) { + super(context, attrs); + //Log.i("frozen-bubble", "MultiplayerGameView constructor"); + init(context, (int) VirtualInput.PLAYER1, FrozenBubble.LOCALE_LOCAL); + } + + /** + * MultiplayerGameView class constructor. + * @param context - the application context. + * @param myPlayerId - the local player ID (1 or 2). + * @param gameLocale - the game topology, which can be either local, + * or distributed over various network types. + */ + public MultiplayerGameView(Context context, int myPlayerId, int gameLocale) { super(context); - //Log.i("frozen-bubble", "GameView constructor"); + //Log.i("frozen-bubble", "MultiplayerGameView constructor"); + init(context, myPlayerId, gameLocale); + } + /** + * MultiplayerGameView object initialization. + * @param context - the application context. + * @param myPlayerId - the local player ID (1 or 2). + * @param gameLocale - the game topology, which can be either local, + * or distributed over various network types. + */ + private void init(Context context, int myPlayerId, int gameLocale) { mContext = context; SurfaceHolder holder = getHolder(); holder.addCallback(this); - mOpponent = null; - // TODO: save and restore the number of games won. + numPlayer1GamesWon = 0; numPlayer2GamesWon = 0; - modeKeyPressed = false; - pauseKeyPressed = false; - soundKeyPressed = false; + /* + * If this game is being played purely locally, then the opponent is + * CPU controlled. Otherwise the opponent is a remote player. + * + * TODO: add the ability to support multiple local players via + * multi-touch, and the ability to specify any player as CPU + * controlled. + */ + boolean isCPU; + boolean isRemote; - mGameThread = new MultiplayerGameThread(holder); + if (gameLocale == FrozenBubble.LOCALE_LOCAL) { + isCPU = true; + isRemote = false; + } + else { + isCPU = false; + isRemote = true; + } + + if (myPlayerId == VirtualInput.PLAYER1) { + mPlayer1 = new PlayerInput(VirtualInput.PLAYER1, false, false); + mPlayer2 = new PlayerInput(VirtualInput.PLAYER2, isCPU, isRemote); + mLocalInput = mPlayer1; + mRemoteInput = mPlayer2; + } + else { + mPlayer1 = new PlayerInput(VirtualInput.PLAYER1, isCPU, isRemote); + mPlayer2 = new PlayerInput(VirtualInput.PLAYER2, false, false); + mLocalInput = mPlayer2; + mRemoteInput = mPlayer1; + } + + /* + * Create a network game manager if this is a network game. + */ + mNetworkManager = null; + if ((gameLocale == FrozenBubble.LOCALE_LAN) || + (gameLocale == FrozenBubble.LOCALE_INTERNET)) { + connectEnum connectType; + if (gameLocale == FrozenBubble.LOCALE_LAN) { + connectType = connectEnum.UDP_MULTICAST; + } + else { + connectType = connectEnum.UDP_UNICAST; + } + mNetworkManager = new NetworkGameManager(context, + connectType, + mLocalInput, + mRemoteInput); + remoteInterface = mNetworkManager.getRemoteInterface(); + } + + /* + * Give this view focus-ability for improved compatibility with + * various input devices. + */ setFocusable(true); setFocusableInTouchMode(true); + /* + * Create and start the game thread. + */ + mGameThread = new MultiplayerGameThread(holder); mGameThread.setRunning(true); mGameThread.start(); } @@ -1506,13 +2313,13 @@ public MultiplayerGameThread getThread() { @Override public boolean onKeyDown(int keyCode, KeyEvent msg) { - //Log.i("frozen-bubble", "GameView.onKeyDown()"); + //Log.i("frozen-bubble", "MultiplayerGameView.onKeyDown()"); return mGameThread.doKeyDown(keyCode, msg); } @Override public boolean onKeyUp(int keyCode, KeyEvent msg) { - //Log.i("frozen-bubble", "GameView.onKeyUp()"); + //Log.i("frozen-bubble", "MultiplayerGameView.onKeyUp()"); return mGameThread.doKeyUp(keyCode, msg); } @@ -1530,34 +2337,54 @@ public boolean onTouchEvent(MotionEvent event) { @Override public void onWindowFocusChanged(boolean hasWindowFocus) { - //Log.i("frozen-bubble", "GameView.onWindowFocusChanged()"); + //Log.i("frozen-bubble", "MultiplayerGameView.onWindowFocusChanged()"); if (!hasWindowFocus) { + if (mNetworkManager != null) { + mNetworkManager.pause(); + } if (mGameThread != null) mGameThread.pause(); } + else { + if (mNetworkManager != null) { + mNetworkManager.unPause(); + } + } } public void surfaceChanged(SurfaceHolder holder, int format, int width, - int height) { - //Log.i("frozen-bubble", "GameView.surfaceChanged"); + int height) { + //Log.i("frozen-bubble", "MultiplayerGameView.surfaceChanged"); mGameThread.setSurfaceSize(width, height); } public void surfaceCreated(SurfaceHolder holder) { - //Log.i("frozen-bubble", "GameView.surfaceCreated()"); + //Log.i("frozen-bubble", "MultiplayerGameView.surfaceCreated()"); mGameThread.setSurfaceOK(true); } public void surfaceDestroyed(SurfaceHolder holder) { - //Log.i("frozen-bubble", "GameView.surfaceDestroyed()"); + //Log.i("frozen-bubble", "MultiplayerGameView.surfaceDestroyed()"); mGameThread.setSurfaceOK(false); } public void cleanUp() { - //Log.i("frozen-bubble", "GameView.cleanUp()"); - mOpponent.stopThread(); + //Log.i("frozen-bubble", "MultiplayerGameView.cleanUp()"); + cleanUpNetworkManager(); + + mPlayer1.init(); + mPlayer2.init(); + + if (mOpponent != null) + mOpponent.stopThread(); mOpponent = null; + mGameThread.cleanUp(); - mContext = null; + } + + private void cleanUpNetworkManager() { + if (mNetworkManager != null) + mNetworkManager.cleanUp(); + mNetworkManager = null; } } diff --git a/src/org/jfedor/frozenbubble/PenguinSprite.java b/src/org/jfedor/frozenbubble/PenguinSprite.java index 8f3e86a..47f409f 100644 --- a/src/org/jfedor/frozenbubble/PenguinSprite.java +++ b/src/org/jfedor/frozenbubble/PenguinSprite.java @@ -122,15 +122,6 @@ public void saveState(Bundle map, Vector saved_sprites, int id) { nextPosition); } - public static Rect getPenguinRect(int player) { - if (player == 1) - return new Rect(361, 436, 361 + PenguinSprite.PENGUIN_WIDTH - 2, - 436 + PenguinSprite.PENGUIN_HEIGHT - 2); - else - return new Rect(221, 436, 221 + PenguinSprite.PENGUIN_WIDTH - 2, - 436 + PenguinSprite.PENGUIN_HEIGHT - 2); - } - public int getTypeId() { return Sprite.TYPE_PENGUIN; }