/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package net.puppygames.applet.screens; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.rmi.Naming; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import net.puppygames.applet.AppletHiscoreServerRemote; import net.puppygames.applet.Area; import net.puppygames.applet.Game; import net.puppygames.applet.GameInputStream; import net.puppygames.applet.GameOutputStream; import net.puppygames.applet.HiscoresReturn; import net.puppygames.applet.MiniGame; import net.puppygames.applet.Res; import net.puppygames.applet.RoamingFile; import net.puppygames.applet.Score; import net.puppygames.applet.Screen; import net.puppygames.applet.TickableObject; import net.puppygames.applet.effects.LabelEffect; import net.puppygames.applet.effects.ProgressEffect; import net.puppygames.applet.effects.SFX; import net.puppygames.applet.widgets.TextField; import org.lwjgl.util.Color; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadableRectangle; import com.shavenpuppy.jglib.TextLayout; import com.shavenpuppy.jglib.opengl.ColorUtil; import com.shavenpuppy.jglib.opengl.GLFont; import com.shavenpuppy.jglib.opengl.GLRenderable; import com.shavenpuppy.jglib.opengl.GLTextArea; import com.shavenpuppy.jglib.resources.ColorSequenceResource; import com.shavenpuppy.jglib.resources.MappedColor; import com.shavenpuppy.jglib.sprites.Appearance; import com.shavenpuppy.jglib.sprites.SimpleRenderer; import com.shavenpuppy.jglib.sprites.Sprite; import static org.lwjgl.opengl.GL11.*; /** * $Id: HiscoresScreen.java,v 1.6 2010/08/03 23:43:39 foo Exp $ * * @author $Author: foo $ * @version $Revision: 1.6 $ */ public class HiscoresScreen extends Screen { private static final long serialVersionUID = 1L; /* * Static data */ private static final String REMOTE_ON = "remote_on"; private static final String REMOTE_OFF = "remote_off"; private static final String REGISTERED_COORDS = "registered_coords"; private static final String RANK_COORDS = "rank_coords"; private static final String NAME_COORDS = "name_coords"; private static final String SCORE_COORDS = "score_coords"; /** Singleton */ private static HiscoresScreen instance; /** 100 Rows */ private static final ArrayList<Row> rows = new ArrayList<Row>(100); private static List<Score> allScores; /* * Resource data */ /** Layout */ private int yPos; private int yGap; private int scoresPerPage = 10; private MappedColor progressBackgroundColor = new MappedColor(Color.BLUE); private MappedColor progressBarColor = new MappedColor(Color.WHITE); private String rankFont, nameFont, scoreFont; private MappedColor rankColor, nameColor, scoreColor = new MappedColor(Color.WHITE); /* * Transient data */ private transient Appearance registeredAppearanceResource; private transient GLFont rankFontResource, nameFontResource, scoreFontResource; private transient boolean remoteHiscores; private transient TickableObject hiscoresObject; private transient ReadableRectangle registeredCoords, rankCoords, nameCoords, scoreCoords; /** Score wrapper */ class Row { int pos; String rank, points; GLTextArea rankLabel, pointsLabel; Score score; TextField field; final Color color = new Color(Color.WHITE); Sprite registeredSprite; Row(int pos, Score score) { this.score = score; this.pos = pos; rank = String.valueOf(score.getRank() + 1); points = String.valueOf(score.getPoints()); if (registeredAppearanceResource != null) { registeredSprite = allocateSprite(HiscoresScreen.this); if (registeredSprite != null) { registeredSprite.setVisible(score.isRegistered()); registeredSprite.setAppearance(registeredAppearanceResource); registeredSprite.setLayer(4); } } rankLabel = new GLTextArea(); rankLabel.setHorizontalAlignment(TextLayout.RIGHT); rankLabel.setVerticalAlignment(GLTextArea.TOP); rankLabel.setFont(rankFontResource); rankLabel.setText(rank); rankLabel.setColour(rankColor); pointsLabel = new GLTextArea(); pointsLabel.setHorizontalAlignment(TextLayout.RIGHT); pointsLabel.setVerticalAlignment(GLTextArea.TOP); pointsLabel.setFont(scoreFontResource); pointsLabel.setText(points); pointsLabel.setColour(scoreColor); field = new TextField(24, nameCoords.getWidth()) { @Override protected void onEdited() { SFX.keyTyped(); } @Override protected void onChangeFocus() { SFX.textEntered(); setKeyboardNavigationEnabled(true); Row.this.score.setName(field.getText().trim()); submitLocalScore(Row.this.score); if (MiniGame.getSubmitRemoteHiscores()) { submitRemoteScore(Row.this.score); } } }; field.setAllCaps(true); field.setText(score.getName().toUpperCase()); field.setFont(nameFontResource); field.setColour(nameColor); onResized(); } void onResized() { int rowYPos = rankCoords.getY() + yPos - pos * yGap; if (registeredSprite != null && registeredCoords != null) { registeredSprite.setLocation(registeredCoords.getX(), registeredCoords.getY() + rowYPos); } rankLabel.setBounds(rankCoords.getX(), rowYPos - Res.getSmallFont().getDescent(), rankCoords.getWidth(), Res.getSmallFont().getHeight()); pointsLabel.setBounds(scoreCoords.getX(), rowYPos - Res.getSmallFont().getDescent(), scoreCoords.getWidth(), Res.getSmallFont().getHeight()); field.setLocation(nameCoords.getX(), nameCoords.getY() + yPos - pos * yGap); field.setWidth(nameCoords.getWidth()); } void render(SimpleRenderer renderer) { renderer.glRender(new GLRenderable() { @Override public void render() { glPushMatrix(); glTranslatef(1.0f, -1.0f, 0.0f); } }); renderer.glColor4f(0.0f, 0.0f, 0.0f, 0.5f); rankLabel.render(renderer); pointsLabel.render(renderer); field.render(renderer); renderer.glRender(new GLRenderable() { @Override public void render() { glPopMatrix(); } }); ColorUtil.setGLColorPre(color, renderer); } void tick() { field.tick(); } void remove() { if (registeredSprite != null) { registeredSprite.deallocate(); registeredSprite = null; } } } /** Score to submit */ private transient Score scoreToSubmit; /** Whether to submit */ private transient boolean doSubmit; /** Current page */ private transient int page; /* * Phases */ private static final int PHASE_NORMAL = 0; private static final int PHASE_EDIT = 1; private static final int PHASE_SUBMIT = 2; private static final int PHASE_DOWNLOAD = 3; /** Buttons */ private static final String NEXT = "next"; private static final String PREV = "prev"; /** Current phase */ private int phase; /** Message */ private ProgressEffect progress; /** Color sequence tick */ private int colorTick; /** Delay */ private int tick; private static final int DELAY = 60; private static final ColorSequenceResource COLORSEQUENCE = new ColorSequenceResource( new ColorSequenceResource.SequenceEntry[] { new ColorSequenceResource.SequenceEntry(Color.RED, 4, 4), new ColorSequenceResource.SequenceEntry(Color.ORANGE, 4, 4), new ColorSequenceResource.SequenceEntry(Color.YELLOW, 4, 4), new ColorSequenceResource.SequenceEntry(Color.GREEN, 4, 4), new ColorSequenceResource.SequenceEntry(Color.CYAN, 4, 4), new ColorSequenceResource.SequenceEntry(Color.BLUE, 4, 4), new ColorSequenceResource.SequenceEntry(Color.PURPLE, 4, 4) }, ColorSequenceResource.REPEAT); private Row editingRow; /** * C'tor */ public HiscoresScreen(String name) { super(name); } @Override protected void doRegister() { super.doRegister(); instance = this; } @Override protected void doDeregister() { super.doDeregister(); instance = null; } /** * Enable buttons */ protected void enableButtons() { if (page == 0) { setEnabled(PREV, false); } else { setEnabled(PREV, phase == PHASE_NORMAL); } if (page < getNumPages() - 1) { setEnabled(NEXT, phase == PHASE_NORMAL); } else { setEnabled(NEXT, false); } setEnabled(GenericButtons.BUY, phase != PHASE_EDIT); setEnabled(GenericButtons.CREDITS, phase != PHASE_EDIT); setEnabled(GenericButtons.EXIT, phase != PHASE_EDIT); setEnabled(GenericButtons.HELP, phase != PHASE_EDIT); setEnabled(GenericButtons.HISCORES, phase != PHASE_EDIT); setEnabled(GenericButtons.MOREGAMES, phase != PHASE_EDIT); setEnabled(GenericButtons.OPTIONS, phase != PHASE_EDIT); setEnabled(GenericButtons.PLAY, phase != PHASE_EDIT); setEnabled(GenericButtons.CLOSE, true); setEnabled(REMOTE_ON, phase != PHASE_EDIT); setEnabled(REMOTE_OFF, phase != PHASE_EDIT); setVisible(REMOTE_ON, MiniGame.getSubmitRemoteHiscores() && remoteHiscores); setVisible(REMOTE_OFF, MiniGame.getSubmitRemoteHiscores() && !remoteHiscores); } @Override protected void onClicked(String id) { GenericButtonHandler.onClicked(id); if (PREV.equals(id)) { if (phase == PHASE_EDIT) { return; } GenericButtonHandler.onClicked(id); setPage(Math.max(0, page - 1)); } else if (NEXT.equals(id)) { if (phase == PHASE_EDIT) { return; } GenericButtonHandler.onClicked(id); setPage(Math.min(getNumPages() - 1, page + 1)); } else if (REMOTE_ON.equals(id)) { if (phase == PHASE_EDIT) { return; } GenericButtonHandler.onClicked(id); setShowRemote(false); } else if (REMOTE_OFF.equals(id)) { if (phase == PHASE_EDIT) { return; } GenericButtonHandler.onClicked(id); setShowRemote(true); } } /** * Sets whether to show remote hiscores or not * @param flag */ private void setShowRemote(boolean flag) { if (remoteHiscores != flag) { remoteHiscores = flag; loadHiscores(); } enableButtons(); } private void loadHiscores() { if (remoteHiscores) { loadRemoteScores(); } else { loadLocalScores(); } } private void loadRemoteScores() { // Download more scores phase = PHASE_DOWNLOAD; progress = new ProgressEffect(Game.getMessage("lwjglapplets.hiscoresscreen.downloadprogress"), progressBackgroundColor, progressBarColor); progress.spawn(this); new Thread() { @Override public void run() { try { AppletHiscoreServerRemote server = (AppletHiscoreServerRemote) Naming.lookup("//"+AppletHiscoreServerRemote.REMOTE_HOST+"/"+AppletHiscoreServerRemote.REMOTE_NAME); List<Score> scores = server.getHiscores(Game.getTitle()); Game.onRemoteCallSuccess(); setScoreList(scores); } catch (Exception e) { Res.getErrorDialog().doModal(Game.getMessage("lwjglapplets.hiscoresscreen.problems"), Game.getMessage("lwjglapplets.hiscoresscreen.unavailable")+" ("+e+")"); } finally { phase = PHASE_NORMAL; synchronized (HiscoresScreen.this) { tick = 0; if (progress != null) { progress.setFinished(true); progress = null; } } enableButtons(); } } }.start(); } private void loadLocalScores() { GameInputStream gis = null; ObjectInputStream ois = null; try { RoamingFile hiscoresFile = new RoamingFile(getHiscoreFileName()); if (hiscoresFile.exists()) { gis = new GameInputStream(getHiscoreFileName()); ois = new ObjectInputStream(gis); setScoreList((ArrayList<Score>) ois.readObject()); // warning suppressed } else { setScoreList(new ArrayList<Score>(0)); } } catch (Exception e) { e.printStackTrace(System.err); setScoreList(new ArrayList<Score>(0)); } finally { if (gis != null) { try { gis.close(); } catch (Exception e) {} } } } /** * @return the filename to use for local hiscores */ private String getHiscoreFileName() { return Game.getRoamingDirectoryPrefix()+"hiscores.dat"; } /** * Set the currently displayed page * @param newPage */ public void setPage(int newPage) { this.page = newPage; enableButtons(); } /** * @return the number of hiscore pages (minimum 1) */ protected final int getNumPages() { return (rows.size() - 1) / scoresPerPage + 1; } @Override protected void doTick() { if (editingRow != null) { COLORSEQUENCE.getColor(colorTick ++, editingRow.color); } for (int i = 0; i < rows.size(); i ++) { Row row = rows.get(i); row.tick(); } } @Override protected void doCleanup() { if (hiscoresObject != null) { hiscoresObject.remove(); hiscoresObject = null; } for (Iterator<Row> i = rows.iterator(); i.hasNext(); ) { Row row = i.next(); row.remove(); } rows.clear(); } /** * Show the hiscore screen. * @param score The player's score, if a new hiscore is being entered */ public static void show(Score score) { if (score != null && score.getPoints() > 0) { instance.doSubmit = true; } else { instance.doSubmit = false; } instance.scoreToSubmit = score; instance.open(); } @Override protected void onClose() { Game.setPauseEnabled(true); } @Override protected synchronized void onOpen() { // Always put hiscores in local hiscore table remoteHiscores = false; enableButtons(); GenericButtonHandler.onOpen(this); tick = 0; colorTick = 0; Game.setPauseEnabled(false); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); loadHiscores(); // Rendering is now done with a TickableObject hiscoresObject = new TickableObject() { @Override public void tick() { for (int i = 0; i < rows.size(); i ++) { Row row = rows.get(i); if (i >= page * scoresPerPage && i < page * scoresPerPage + scoresPerPage) { if (row.registeredSprite != null) { row.registeredSprite.setVisible(row.score.isRegistered()); } } else { if (row.registeredSprite != null) { row.registeredSprite.setVisible(false); } } } } @Override protected void render() { glRender(new GLRenderable() { @Override public void render() { glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); } }); for (int i = 0; i < rows.size(); i ++) { Row row = rows.get(i); if (i >= page * scoresPerPage && i < page * scoresPerPage + scoresPerPage) { row.render(this); } } } }; hiscoresObject.setLayer(100); hiscoresObject.spawn(this); } private synchronized void setScoreList(List<Score> scores) { allScores = scores; for (Iterator<Row> i = rows.iterator(); i.hasNext(); ) { Row row = i.next(); row.remove(); } rows.clear(); // Create pages of Rows. If we've got a score to submit, we will insert // an editable row somewhere in there as well, which means we'll have to // adjust all the ranks of subsequent scores int rank = 0; int page = 0, editingPage = 0; int pos = 0; boolean done = false; phase = PHASE_NORMAL; for (Iterator<Score> i = allScores.iterator(); i.hasNext(); ) { Score s = i.next(); if (doSubmit && scoreToSubmit != null && scoreToSubmit.compareTo(s) == -1 && !done) { // We've got a hiscore to submit, and it's beaten this score! So this row // is the editing row. scoreToSubmit.setRank(rank ++); Row row = new Row(pos, scoreToSubmit); row.field.setEditing(true); setKeyboardNavigationEnabled(false); rows.add(row); editingPage = page; done = true; pos ++; if (pos == scoresPerPage) { pos = 0; page ++; } phase = PHASE_EDIT; } s.setRank(rank ++); Row row = new Row(pos, s); rows.add(row); pos ++; if (pos == scoresPerPage) { pos = 0; page ++; } } // So we've got to the end.. perhaps we've not managed to find a slot? if (doSubmit && scoreToSubmit != null && !done && rank < AppletHiscoreServerRemote.MAX_SCORES) { scoreToSubmit.setRank(rank ++); Row row = new Row(pos, scoreToSubmit); row.field.setEditing(true); setKeyboardNavigationEnabled(false); rows.add(row); editingPage = page; phase = PHASE_EDIT; } setPage(editingPage); } private void submitRemoteScore(final Score score) { phase = PHASE_SUBMIT; progress = new ProgressEffect(Game.getMessage("lwjglapplets.hiscoresscreen.submitting"), progressBackgroundColor, progressBarColor); progress.spawn(instance); enableButtons(); new Thread() { @Override public void run() { try { AppletHiscoreServerRemote server = (AppletHiscoreServerRemote) Naming.lookup("//"+AppletHiscoreServerRemote.REMOTE_HOST+"/"+AppletHiscoreServerRemote.REMOTE_NAME); doSubmit = false; HiscoresReturn ret = server.submit2(score); if (remoteHiscores) { setScoreList(ret.getScores()); } if (ret.getMessage() != null) { StringTokenizer st = new StringTokenizer(ret.getMessage(), "\n", false); List<String> tokens = new LinkedList<String>(); while (st.hasMoreTokens()) { tokens.add(st.nextToken()); } int h = tokens.size() * Res.getBigFont().getHeight(); int y = (Game.getHeight() - h) / 2 + (tokens.size() - 1) * Res.getBigFont().getHeight(); int delay = 0; for (Iterator<String> i = tokens.iterator(); i.hasNext(); ) { LabelEffect le = new LabelEffect(Res.getBigFont(), i.next(), ReadableColor.WHITE, ReadableColor.CYAN, 240, 120); le.setLocation(Game.getWidth() / 2, y); y -= Res.getBigFont().getHeight(); le.setDelay(delay); delay += 30; le.setSound(SFX.getTextEnteredBuffer()); le.spawn(instance); } } Game.onRemoteCallSuccess(); } catch (SQLException e) { e.printStackTrace(System.err); Game.onRemoteCallSuccess(); // The actual call succeeded Res.getErrorDialog().doModal(Game.getMessage("lwjglapplets.hiscoresscreen.problems"), e.getMessage()); } catch (Exception e) { e.printStackTrace(System.err); Res.getErrorDialog().doModal("PROBLEMS", e.getMessage()); } finally { phase = PHASE_NORMAL; progress.setFinished(true); progress = null; tick = 0; enableButtons(); } } }.start(); } private void submitLocalScore(Score score) { doSubmit = false; allScores.add(score); Collections.sort(allScores); GameOutputStream gos = null; ObjectOutputStream oos = null; try { gos = new GameOutputStream(getHiscoreFileName()); oos = new ObjectOutputStream(gos); oos.writeObject(allScores); oos.flush(); gos.flush(); phase = PHASE_NORMAL; } catch (Exception e) { e.printStackTrace(System.err); Res.getErrorDialog().doModal(Game.getMessage("lwjglapplets.hiscoresscreen.problems"), Game.getMessage("lwjglapplets.hiscoresscreen.problemsaving")+" ("+e+")"); } finally { if (oos != null) { try { oos.close(); } catch (Exception e) {} } if (gos != null) { try { gos.close(); } catch (Exception e) {} } enableButtons(); } } @Override protected void onResized() { Area registeredArea = getArea(REGISTERED_COORDS); if (registeredArea != null) { registeredCoords = registeredArea.getBounds(); } rankCoords = getArea(RANK_COORDS).getBounds(); nameCoords = getArea(NAME_COORDS).getBounds(); scoreCoords = getArea(SCORE_COORDS).getBounds(); for (Row row : rows) { row.onResized(); } } }