/**
*
*/
package net.puppygames.applet;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.prefs.BackingStoreException;
import net.puppygames.applet.effects.EffectFeature;
import net.puppygames.applet.effects.SFX;
import net.puppygames.applet.screens.CreditsScreen;
import net.puppygames.applet.screens.DialogScreen;
import net.puppygames.applet.screens.HiscoresScreen;
import net.puppygames.applet.screens.InstructionsScreen;
import net.puppygames.applet.screens.NagScreen;
import net.puppygames.applet.screens.RegisterScreen;
import net.puppygames.applet.screens.SignUpScreen;
import net.puppygames.applet.screens.TitleScreen;
import net.puppygames.applet.screens.UnlockBonusScreen;
import net.puppygames.gamecommerce.shared.NewsletterIncentive;
import org.lwjgl.Sys;
import com.shavenpuppy.jglib.Resources;
import com.shavenpuppy.jglib.resources.TextResource;
/**
* Puppygames MiniGames, which has basic state management for our simple arcade games.
*/
public abstract class MiniGame extends Game {
private static final boolean TESTSIGNUP = true;
private static final boolean IGNORESIGNUP = false;
private static final String RESTORE_GAME_DIALOG_FEATURE = "restore_game.dialog";
private static final String SAVE_GAME_EFFECT_FEATURE = "save_game.effect";
/** Games this session */
private static int playedThisSession;
/** Instructions shown? */
private static boolean shownInstructions;
/** Ticks played so far */
private static int playedTicks;
/** Prevent the Buy page appearing */
private static boolean preventBuy;
/** Do the Buy page */
private static boolean doBuy;
/** Game state */
private static GameState gameState;
/** Allow saving the game */
private static boolean allowSave = true;
/** Restore game dialog */
private static DialogScreen restoreGameDialog;
/** Top screen */
private static Screen topScreen;
/** Score group */
private static String scoreGroup;
/** Prize to be redeemed on starting a new game */
private static PrizeFeature prize;
/*
* Resource data
*/
/** Don't allow remote hiscores */
private boolean dontUseRemoteHiscores;
/*
* Transient data
*/
/** Whether to submit hiscores */
private transient boolean submitRemoteHiscores;
/**
* Begin a new game
*/
public static void beginNewGame() {
System.out.println("Begin new game");
// If demo version, count down the number of plays
if (!Game.isRegistered()) {
// If not played before and not registered, open help first
if (!shownInstructions && MiniGame.maybeShowHelp()) {
return;
}
int played = getLocalPreferences().getInt("played" + Game.getVersion(), 0);
played ^= 0xAF6AD755;
played = played >> 16 & 0xFFFF | played << 16;
played ^= 0xCCCCCABE;
if (played == 0x1B9965D4) {
played = 0;
}
int tix = getLocalPreferences().getInt("tix", 0);
System.out.println("You have played " + Game.getTitle() + " " + played + " times for " + tix / 60 + " minutes");
System.out.println("Max games " + Game.getConfiguration().getMaxGames() + " / max time " + Game.getConfiguration().getMaxTime() / 60 + " / max level " + Game.getConfiguration().getMaxLevel());
if (MiniGame.isDemoExpired() && (Game.getConfiguration().isCrippled() || playedThisSession > 0)) {
// Nag and quit on second game this session
NagScreen.show("Your demo has expired!", true);
return;
} else {
played++;
played ^= 0xCCCCCABE;
played = played >> 16 & 0xFFFF | played << 16;
played ^= 0xAF6AD755;
getLocalPreferences().putInt("played" + Game.getVersion(), played);
flushPrefs();
Game.getGameInfo().onNewGame();
playedThisSession++;
}
}
SFX.newGame();
getGame().onBeginNewGame();
}
public static MiniGame getGame() {
return (MiniGame) Game.getGame();
}
/**
* Buy the game
*/
public static void buy(boolean doExit) {
if (!doBuy) {
doBuy = true;
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if (Game.isRegistered() || preventBuy) {
return;
}
Game.getLocalPreferences().putBoolean("showregister", true);
try {
String page;
if (Resources.exists("buy_url")) {
TextResource tr = (TextResource) Resources.get("buy_url");
page = tr.getText();
} else if (Game.getBuyURL() != null && !"".equals(Game.getBuyURL())) {
page = Game.getBuyURL();
} else if (!System.getProperty("buy_url", "!").equals("!")) {
page = System.getProperty("buy_url");
} else {
page = "http://" + Game.getWebsite() + "/purchase/buy.php?game=" + URLEncoder.encode(Game.getTitle(), "utf-8") + "&configuration=" + URLEncoder.encode(Game.configuration.encode(), "utf-8") + "@installation@";
}
String replacement = "&installation=" + URLEncoder.encode(String.valueOf(Game.installation), "utf-8");
int idx = page.indexOf("@installation@");
if (idx != -1) {
StringBuilder sb = new StringBuilder(page);
sb.replace(idx, idx + 14, replacement);
page = sb.toString();
}
if (!Sys.openURL(page)) {
throw new Exception("Failed to open URL "+page);
}
} catch (Exception e) {
e.printStackTrace(System.err);
Game.alert("Please open your web browser on the page http://" + Game.getWebsite());
}
}
});
}
if (doExit) {
Game.exit();
}
}
/**
* Clear the buy flag
*/
public static void clearBuy() {
preventBuy = true;
}
/**
* End the game and return to the title screen (or the hiscore entry screen)
*/
public static void endGame() {
getGame().updateLog();
getGame().doEndGame();
}
/**
* Game over
*/
public static void gameOver() {
SFX.gameOver();
getGame().doGameOver();
}
/**
* @return true if the demo expired
*/
public static boolean isDemoExpired() {
return getGame().doIsDemoExpired();
}
/**
* Is there a game to restore?
* @return boolean
*/
public static boolean isRestoreAvailable() {
return new RoamingFile(getGame().getRestoreFile()).exists();
}
public static boolean maybeShowHelp() {
return getGame().doMaybeShowHelp();
}
/**
* Call this every frame that the game is being played.
*/
public static void onTicked() {
playedTicks++;
}
/**
* Restore the game
*/
public static void restoreGame() {
getGame().onRestoreGame();
}
/**
* Save the game
*/
public static void saveGame() {
getGame().doSaveGame();
}
/**
* Show the credits screen (if any)
*/
public static void showCredits() {
getGame().doShowCredits();
}
/**
* Show the help screen (if any)
*/
public static void showHelp() {
shownInstructions = true;
getGame().doShowHelp();
}
/**
* Show the hiscores screen (if any)
*/
public static void showHiscores() {
getGame().doShowHiscores();
}
/**
* Show the "More Games" URL
*/
public static void showMoreGames() {
new Thread() {
@Override
public void run() {
try {
String page;
getLocalPreferences().putBoolean("showregister", true);
if (Resources.exists("moregames_url")) {
TextResource tr = (TextResource) Resources.get("moregames_url");
page = tr.getText();
} else if (getMoreGamesURL() != null && !"".equals(getMoreGamesURL())) {
page = getMoreGamesURL();
} else if (!System.getProperty("moregames_url", "!").equals("!")) {
page = System.getProperty("moregames_url");
} else {
page = "http://" + Game.getWebsite();
}
if (!Sys.openURL(page)) {
throw new Exception("Failed to open URL "+page);
}
} catch (Exception e) {
e.printStackTrace(System.err);
Game.alert("Please open your web browser on the page http://" + Game.getWebsite());
}
}
}.start();
}
/**
* Show the options screen (if any)
*/
public static void showOptions() {
getGame().doShowOptions();
}
/**
* Show the registration screen (if any)
*/
public static void showRegisterScreen() {
getGame().doShowRegisterScreen();
}
/**
* Show the title screen (if any)
*/
public static void showTitleScreen() {
getGame().doShowTitleScreen();
}
/**
* Write tix out so we can count how long the player has played for
*/
@Override
protected void updateLog() {
// Write tix
int tix = getLocalPreferences().getInt("tix", 0);
int newTix = playedTicks / getFrameRate();
tix += newTix;
playedTicks = 0;
getGameInfo().addTime(newTix);
getLocalPreferences().putInt("tix", tix);
flushPrefs();
}
/**
* C'tor
* @param name
*/
public MiniGame(String name) {
super(name);
}
/**
* End the game and return to the title screen (or the hiscore entry screen). The default is simply to return to the title
* screen.
*/
protected void doEndGame() {
MiniGame.showTitleScreen();
}
/**
* Game over. This should put the game in a "dormant" state and display the "game over" message to the player. After a while
* someone will call endGame() and put us back on the title screen.
*/
protected abstract void doGameOver();
protected boolean doIsDemoExpired() {
if (isRegistered()) {
return false;
}
int played = getLocalPreferences().getInt("played" + getVersion(), 0);
played ^= 0xAF6AD755;
played = played >> 16 & 0xFFFF | played << 16;
played ^= 0xCCCCCABE;
if (played == 0x1B9965D4) {
played = 0;
}
int tix = getLocalPreferences().getInt("tix", 0);
return played < 0 || (played > configuration.getMaxGames() && configuration.getMaxGames() > 0 || configuration.getMaxGames() == 0) && (tix > configuration.getMaxTime() && configuration.getMaxTime() > 0 || configuration.getMaxTime() == 0);
}
/**
* @return true if help is shown; false if you just want to start the game
*/
protected boolean doMaybeShowHelp() {
MiniGame.showHelp();
return true;
}
/**
* Show credits screen (if any).
*/
protected void doShowCredits() {
CreditsScreen.show();
}
/**
* Show help screen (if any).
*/
protected void doShowHelp() {
InstructionsScreen.show();
}
/**
* Show hiscores screen (if any).
*/
protected void doShowHiscores() {
HiscoresScreen.show(null);
}
/**
* Show options screen (if any). Default is do nothing.
*/
protected void doShowOptions() {
}
/**
* Show the registration screen (if any)
*/
protected void doShowRegisterScreen() {
RegisterScreen.show();
}
/**
* Show the title screen (if any)
*/
protected void doShowTitleScreen() {
TitleScreen.show();
}
/**
* Called to begin a new game.
*/
protected void onBeginNewGame() {
if (MiniGame.isRestoreAvailable()) {
// Ask to resume
MiniGame.restoreGame();
} else {
cleanGame();
}
}
@Override
protected void onExit() {
// And nag :)
if (getGame() != null && !preventBuy) {
buy(false);
}
}
/**
* Restore the game
*/
protected void doRestoreGame() {
allowSave = true;
String file = getRestoreFile();
GameInputStream gis = null;
ObjectInputStream ois = null;
boolean exceptionOccurred = false;
try {
gis = new GameInputStream(file);
ois = new ObjectInputStream(gis);
setGameState((GameState) ois.readObject());
// Check tox value in prefs...
long tox;
if (getPlayerSlot() != null) {
tox = getPlayerSlot().getPreferences().getLong(getSaveGameRegistryMagicLocation(), 0L);
} else {
tox = getRoamingPreferences().getLong(getSaveGameRegistryMagicLocation(), 0L);
}
if (!DEBUG && tox != gameState.getMagic()) {
throw new Exception("Invalid game state");
}
Resources.dequeue();
gameState.reinit();
} catch (Exception e) {
exceptionOccurred = true;
e.printStackTrace(System.err);
onRestoreGameFailed(e);
} finally {
if (gis != null) {
try {
gis.close();
} catch (Exception e) {
}
}
if (!Game.DEBUG) {
// Vape tox value
if (getPlayerSlot() != null) {
getPlayerSlot().getPreferences().putLong("tox", new Random().nextLong());
} else {
getRoamingPreferences().putLong("tox", new Random().nextLong());
}
flushPrefs();
// Now delete the file whatever happens. If an exception occurs, rename it instead, if we can, or delete it, if we
// can't
if (exceptionOccurred) {
if (isUsingSteamCloud()) {
// Nothing we can do with Steam.
if (!new RoamingFile(file).delete()) {
System.err.println("Failed to delete save file");
}
} else {
File newFile = new File(file+".broken");
if (!new File(file).renameTo(newFile)) {
System.err.println("Failed to rename "+file+" to "+newFile);
if (!new RoamingFile(file).delete()) {
System.err.println("Failed to delete save file");
}
}
}
} else if (!new RoamingFile(file).delete()) {
System.err.println("Failed to delete save file");
}
}
}
}
/**
* Set game state
* @param newgameState
*/
protected void setGameState(GameState newGameState) {
MiniGame.gameState = newGameState;
}
/**
* Factory method to create a GameState
* @return a GameState
*/
protected abstract GameState createGameState();
/**
* Choose a random valid prize
* @return a {@link PrizeFeature}, or null
*/
private static PrizeFeature choosePrize() {
List<PrizeFeature> prizes = new ArrayList<PrizeFeature>(PrizeFeature.getPrizes());
Collections.shuffle(prizes);
for (PrizeFeature pf : prizes) {
if (pf.isValid()) {
return pf;
}
}
return null;
}
public static NagState getNagState() {
return NagState.valueOf(getRoamingPreferences().get("nagstate", NagState.NOT_YET_SHOWN.name()));
}
public static void setNagState(NagState newNagState) {
getRoamingPreferences().put("nagstate", newNagState.name());
flushPrefs();
}
@SuppressWarnings("unused")
@Override
protected void onPreRegisteredStartup() {
if (IGNORESIGNUP && DEBUG) {
showTitleScreen();
return;
}
NagState nagState = getNagState();
if (TESTSIGNUP && DEBUG) {
nagState = NagState.NOT_YET_SHOWN;
}
switch (nagState) {
case NOT_YET_SHOWN:
PrizeFeature prize = choosePrize();
if (prize != null) {
SignUpScreen.show(prize);
} else {
showTitleScreen();
}
break;
case PRIZE_AWAITS:
// Restore the incentive file
FileInputStream fis = null;
BufferedInputStream bis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream(getIncentiveFile());
bis = new BufferedInputStream(fis);
ois = new ObjectInputStream(bis);
NewsletterIncentive ni = (NewsletterIncentive) ois.readObject();
if (!ni.validate()) {
throw new Exception("Existing incentive file is invalid.");
}
UnlockBonusScreen.show(ni);
} catch (Exception e) {
e.printStackTrace(System.err);
showTitleScreen();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
}
}
}
break;
case DONT_NAG:
case REDEEMED:
showTitleScreen();
break;
default:
assert false : "Unknown nag state "+nagState;
}
}
public static File getIncentiveFile() {
return new File(getRoamingDirectoryPrefix() + "incentive.dat");
}
public static void setPrize(PrizeFeature prize) {
MiniGame.prize = prize;
}
public static PrizeFeature getPrize() {
return prize;
}
/**
* Asks the user if they want to restore their game, and then either restores it, starts a clean game, or does nothing.
*/
protected void onRestoreGame() {
if (!isRestoreAvailable()) {
return;
}
topScreen = Screen.getTopScreen();
if (topScreen == null) {
return;
}
try {
restoreGameDialog = (DialogScreen) Resources.get(RESTORE_GAME_DIALOG_FEATURE);
restoreGameDialog.doModal(getMessage("lwjglapplets.minigame.restoregame.title"), getMessage("lwjglapplets.minigame.restoregame.message"), new Runnable() {
@Override
public void run() {
topScreen.setEnabled(true);
switch (restoreGameDialog.getOption()) {
case DialogScreen.YES_OPTION:
doRestoreGame();
break;
case DialogScreen.NO_OPTION:
cleanGame();
break;
default:
// Do nothing
break;
}
restoreGameDialog = null;
}
});
topScreen.setEnabled(false);
} catch (Exception e) {
e.printStackTrace(System.err);
onRestoreGameFailed(e);
}
}
/**
* Called when a restore game failed
*/
protected void onRestoreGameFailed(Exception e) {
}
protected String getSaveGameRegistryMagicLocation() {
return "tox";
}
protected void doSaveGame() {
if (!allowSave) {
return;
}
String file = getRestoreFile();
try {
GameOutputStream gos = new GameOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(gos);
// Set current magic number
gameState.setMagic(new Random().nextLong());
if (getPlayerSlot() != null) {
getPlayerSlot().getPreferences().putLong(getSaveGameRegistryMagicLocation(), gameState.getMagic());
} else {
getRoamingPreferences().putLong(getSaveGameRegistryMagicLocation(), gameState.getMagic());
}
// This restore file is now only valid when tox in prefs is the
// same as that in the file. As soon as we do a restore, we zap
// the tox value :)
oos.writeObject(gameState);
oos.flush();
gos.close();
allowSave = false;
onGameSaved();
flushPrefs();
} catch (Exception e) {
e.printStackTrace(System.err);
}
TitleScreen.show();
}
/**
* Called when the game has been saved. Use it to pop up some sort of confirmation on the title screen
*/
protected void onGameSaved() {
((EffectFeature) Resources.get(SAVE_GAME_EFFECT_FEATURE)).spawn(TitleScreen.getInstance());
}
/**
* Start a new game from scratch
*/
public static void cleanGame() {
RoamingFile file = new RoamingFile(getGame().getRestoreFile());
if (file.exists() && !file.delete()) {
System.err.println("Failed to delete save file "+file);
}
allowSave = true;
getGame().onCleanGame();
}
/**
* Called to start a new game from scratch after deleting the restore file
*/
protected void onCleanGame() {
setGameState(createGameState());
gameState.init();
}
/**
* @return the path of the file which we use to save games to
*/
protected String getRestoreFile() {
if (getPlayerSlot() != null) {
return getPlayerDirectoryPrefix() + RESTORE_FILE;
} else {
return getRoamingDirectoryPrefix() + RESTORE_FILE;
}
}
/**
* @return Returns the scoreGroup.
*/
public static String getScoreGroup() {
return scoreGroup;
}
/**
* @param scoreGroup The scoreGroup to set.
*/
public static void setScoreGroup(String scoreGroup) {
MiniGame.scoreGroup = scoreGroup;
}
@Override
protected void onInit() {
// Remote hiscores?
getGame().submitRemoteHiscores = getRoamingPreferences().getBoolean("submitremotehiscores", !getGame().dontUseRemoteHiscores);
}
@Override
protected void onRegisteredStartup() {
showTitleScreen();
}
@Override
protected void onUnregisteredStartup() {
showRegisterScreen();
}
public static boolean getDontUseRemoteHiscores() {
return getGame().dontUseRemoteHiscores;
}
public static boolean getSubmitRemoteHiscores() {
return !getGame().dontUseRemoteHiscores && getGame().submitRemoteHiscores;
}
public static void setSubmitRemoteHiscores(boolean set) {
getGame().submitRemoteHiscores = set;
synchronized (getRoamingPreferences()) {
getRoamingPreferences().putBoolean("submitremotehiscores", !getGame().dontUseRemoteHiscores && getGame().submitRemoteHiscores);
}
new Thread() {
@Override
public void run() {
try {
synchronized (getRoamingPreferences()) {
getRoamingPreferences().flush();
}
} catch (BackingStoreException e) {
e.printStackTrace(System.err);
}
}
}.start();
}
}