/* * $Id$ * * Copyright (c) 2008 by Michael Kiefte * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.tools.imports.adc2; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.InputEvent; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.TreeSet; import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import VASSAL.build.Configurable; import VASSAL.build.GameModule; import VASSAL.build.Widget; import VASSAL.build.module.ChartWindow; import VASSAL.build.module.DiceButton; import VASSAL.build.module.GlobalOptions; import VASSAL.build.module.Map; import VASSAL.build.module.MultiActionButton; import VASSAL.build.module.PieceWindow; import VASSAL.build.module.PlayerHand; import VASSAL.build.module.PlayerRoster; import VASSAL.build.module.PrivateMap; import VASSAL.build.module.PrototypeDefinition; import VASSAL.build.module.PrototypesContainer; import VASSAL.build.module.ToolbarMenu; import VASSAL.build.module.map.BoardPicker; import VASSAL.build.module.map.CounterDetailViewer; import VASSAL.build.module.map.DrawPile; import VASSAL.build.module.map.LOS_Thread; import VASSAL.build.module.map.LayeredPieceCollection; import VASSAL.build.module.map.MassKeyCommand; import VASSAL.build.module.map.SetupStack; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.MapGrid; import VASSAL.build.module.map.boardPicker.board.MapGrid.BadCoords; import VASSAL.build.module.map.boardPicker.board.ZonedGrid; import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone; import VASSAL.build.module.turn.ListTurnLevel; import VASSAL.build.module.turn.TurnTracker; import VASSAL.build.widget.CardSlot; import VASSAL.build.widget.Chart; import VASSAL.build.widget.HtmlChart; import VASSAL.build.widget.ListWidget; import VASSAL.build.widget.PieceSlot; import VASSAL.build.widget.TabWidget; import VASSAL.configure.StringArrayConfigurer; import VASSAL.counters.BasicPiece; import VASSAL.counters.Deck; import VASSAL.counters.Decorator; import VASSAL.counters.Delete; import VASSAL.counters.DynamicProperty; import VASSAL.counters.Embellishment; import VASSAL.counters.Footprint; import VASSAL.counters.FreeRotator; import VASSAL.counters.GamePiece; import VASSAL.counters.Hideable; import VASSAL.counters.Marker; import VASSAL.counters.MovementMarkable; import VASSAL.counters.Obscurable; import VASSAL.counters.PropertySheet; import VASSAL.counters.Replace; import VASSAL.counters.ReturnToDeck; import VASSAL.counters.UsePrototype; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.SequenceEncoder; import VASSAL.tools.filechooser.ExtensionFileFilter; import VASSAL.tools.imports.FileFormatException; import VASSAL.tools.imports.Importer; import VASSAL.tools.imports.adc2.SymbolSet.SymbolData; import VASSAL.tools.io.IOUtils; public class ADC2Module extends Importer { private static final String FLIP_DEFINITIONS = "Flip Definitions"; private static final String ADD_NEW_PIECES = "Add New Pieces"; private static final String PC_NAME = "$pcName$"; private static final String CHARTS = "Charts"; private static final String TRAY = "Tray"; private static final String FORCE_POOL_PNG = "forcePool.png"; private static final String FORCE_POOL = "Force Pool"; private static final String DECKS = "Decks"; protected static class ForcePoolList extends ArrayList<Pool> { private static final long serialVersionUID = 1L; private class ForcePoolIterator implements Iterator<Pool> { private final Class<?> type; private int cursor = 0; private ForcePoolIterator(Class<?> type) { this.type = type; setNext(); } private void setNext() { while (cursor < size()) { if (get(cursor).getClass() == type && get(cursor).isUseable()) break; else ++cursor; } } public boolean hasNext() { return cursor < size(); } public Pool next() { Pool p = get(cursor); ++cursor; setNext(); return p; } public void remove() { throw new UnsupportedOperationException(); } } public int count(Class<?> type) { int size = 0; Iterator<Pool> iter = iterator(type); while (iter.hasNext()) { Pool p = iter.next(); if (p.getClass() == type && p.isUseable()) ++size; } return size; } public Iterator<ADC2Module.Pool> iterator(Class<?> type) { return new ForcePoolIterator(type); } } public class Pool { public final String name; public final ArrayList<Piece> pieces; Pool(String name, ArrayList<Piece> pieces) { this.name = name; this.pieces = pieces; } List<Piece> getPieces() { if (pieces == null) return Collections.emptyList(); else return Collections.unmodifiableList(pieces); } String getButtonName() { return name; } boolean isUseable() { return true; } } public class Cards extends Pool { protected Player owner; Cards(String name, ArrayList<Piece> pieces) { super(name, pieces); // cull non-cards if (pieces != null) { for (Iterator<Piece> iter = pieces.iterator(); iter.hasNext(); ) { if (!iter.next().isCard()) { iter.remove(); } } } } public void setOwner(int owner) { if (owner == NO_PLAYERS) this.owner = Player.NO_PLAYERS; else if (owner >= ALL_PLAYERS) this.owner = Player.ALL_PLAYERS; else if (owner >= players.size()) this.owner = Player.UNKNOWN; else this.owner = players.get(owner); } public Player getOwner() { return owner; } } public class DeckPool extends Cards { DeckPool(String name, ArrayList<Piece> pieces) { super(name, pieces); } } public class HandPool extends Cards { HandPool(String name, ArrayList<Piece> pieces) { super(name, pieces); } @Override String getButtonName() { return super.getButtonName() + " (" + getOwner().getName() + " Hand)"; } } public class ForcePool extends Pool { @Override boolean isUseable() { if (getPieces().size() > 0) return true; if (name == null) return false; for (int i = 0; i < name.length(); ++i) { if (Character.isLetterOrDigit(name.charAt(i))) return true; } return false; } ForcePool(String name, ArrayList<Piece> pieces) { super(name, pieces); } } public class StatusDots { // type public static final int NOT_USED = 0; public static final int MOVED = 1; public static final int IN_COMBAT = 2; public static final int ATTACKED = 3; public static final int DEFENDED = 4; public static final int CLASS_VALUE = 5; public static final int PIECE_VALUE = 6; // position public static final int DO_NOT_DRAW = 0; public static final int TOP_LEFT = 1; public static final int TOP_CENTER = 2; public static final int TOP_RIGHT = 3; public static final int CENTER_LEFT = 4; public static final int CENTER_CENTER = 5; public static final int CENTER_RIGHT = 6; public static final int BOTTOM_LEFT = 7; public static final int BOTTOM_CENTER = 8; public static final int BOTTOM_RIGHT = 9; private final int type; private final int show; private final Color color; private final int position; private final int size; protected StatusDots(int type, int show, Color color, int position, int size) { this.type = type; this.show = show; this.color = color; this.position = position; this.size = size; } public Color getColor() { return color; } public int getPosition() { return position; } public int getShow() { return show; } public int getSize() { return size; } public int getType() { return type & 0xf; } public String getStatusPropertyName() { if (getType() == CLASS_VALUE) return classValues[type >>> 4]; else if (getType() == PIECE_VALUE) return pieceValues[type >>> 4]; else return null; } } private static final int FORCE_POOL_BLOCK_END = 30000; public static final String DRAW_ON_TOP_OF_OTHERS = "Draw on top of others?"; public static final String PIECE = "Pieces"; private static final double[] FACING_ANGLES = new double[46]; static { for (int i = 0; i < 3; ++i) { FACING_ANGLES[i+1] = -i*90.0; FACING_ANGLES[i+5] = -(i*90.0 + 45.0); } for (int i = 0; i < 6; ++i) { FACING_ANGLES[i+10] = -i*60.0; FACING_ANGLES[i+20] = -((i*60.0 - 15.0) % 360.0); FACING_ANGLES[i+30] = -(i*60.0 + 30.0); FACING_ANGLES[i+40] = -(i*60.0 + 15.0); } } private final HashSet<String> uniquePieceNames = new HashSet<String>(); private static boolean usePieceNames = false; public class Piece { private static final String PIECE_PROPERTIES = "Piece Properties"; public final PieceClass pieceClass; public final HideState hideState; private final int[] values = new int[8]; private final ValueType[] types = new ValueType[8]; private final String name; private final int flags; private final int facing; private GamePiece gamePiece; private PieceSlot pieceSlot; private final int position; private PropertySheet classPS = null; private PropertySheet piecePS = null; private Marker pieceNameMarker = null; public Piece(PieceClass cl) { this.name = null; this.pieceClass = cl; this.flags = 0; this.hideState = null; this.position = -1; facing = 0; } public Piece(int position, String name, PieceClass cl, HideState hidden, int flags, int facing) { if (name == null || name.equals("")) this.name = null; else this.name = name; this.position = position; this.pieceClass = cl; this.flags = flags; assert(hidden != null); this.hideState = hidden; this.facing = facing; final HashMap<Integer, ArrayList<Piece>> hash; if (inForcePool()) hash = forcePoolHashMap; else hash = stacks; ArrayList<Piece> stack = hash.get(position); if (stack == null) { stack = new ArrayList<Piece>(); stack.add(this); hash.put(position, stack); } else { stack.add(0, this); } } public Pool getForcePool() { if (inForcePool()) { return forcePools.get(position); } else { return null; } } public boolean isCard() { return types[0] == ValueType.CARD; } @Override public boolean equals(Object obj) { if (!(obj instanceof Piece)) return false; if (getUniqueClassName().equals(((Piece) obj).getUniqueClassName()) && pieceClass == ((Piece) obj).pieceClass) return true; else return false; } @Override public int hashCode() { return getUniqueClassName().hashCode(); } protected void setValue(int index, int value) { values[index] = value; types[index] = ValueType.NUMERIC; } protected void writeToArchive(SetupStack parent) throws IOException { final GamePiece gp = getGamePiece(); if (gp == null) return; assert(pieceSlot == null); pieceSlot = new PieceSlot(gp); insertComponent(pieceSlot, parent); } protected void writeToArchive(DrawPile parent) throws IOException { GamePiece gp = getGamePiece(); if (gp == null) return; assert(pieceSlot == null); pieceSlot = new CardSlot(); pieceSlot.setPiece(gp); insertComponent(pieceSlot, parent); } // TODO: create option whereby anyone can flip/hide a card. public Player getPlayer() { return pieceClass.getOwner(); } public boolean inForcePool() { return (flags & 0x8) > 0; } protected GamePiece getGamePiece() throws IOException { if (gamePiece == null) { gamePiece = getBasicPiece(); if (gamePiece == null) return null; // TODO: implement a YES_NO field type for PropertySheets // and a stack property viewer. // Piece values appendDecorator(getPieceNameMarker()); appendDecorator(getDynamicProperty()); appendDecorator(getPieceValueMask()); appendDecorator(getMovementMarkable()); appendDecorator(getDefendedEmbellishment()); appendDecorator(getAttackedEmbellishment()); appendDecorator(getFreeRotator()); appendDecorator(getUsePrototype()); appendDecorator(getPiecePropertySheet()); appendDecorator(getReplaceWithPrevious()); appendDecorator(getReplaceWithOther()); appendDecorator(getClassPropertySheet()); appendDecorator(getHidden()); } return gamePiece; } private Decorator getReplaceWithPrevious() throws IOException { return pieceClass.getReplaceWithPreviousDecorator(); } private Decorator getReplaceWithOther() throws IOException { return pieceClass.getReplaceWithOtherDecorator(); } private Marker getPieceNameMarker() { if (pieceNameMarker == null) { if (name != null && name.length() > 0) { usePieceNames = true; } pieceNameMarker = new Marker(Marker.ID + "pcName", null); final SequenceEncoder se = new SequenceEncoder(','); se.append(name == null ? "" : name); pieceNameMarker.mySetState(se.getValue()); } return pieceNameMarker; } private void appendDecorator(Decorator p) { if (p != null) { p.setInner(gamePiece); gamePiece = p; } } protected Decorator getHidden() throws IOException { Decorator p = pieceClass.getHiddenDecorator(); if (p != null && isHidden()) { Player player = pieceClass.getPlayer(this); if (player != Player.ALL_PLAYERS && player != Player.NO_PLAYERS) { p.mySetState(player.getName()); } } return p; } private boolean isHidden() { return pieceClass.checkHidden(this); } protected Obscurable getPieceValueMask() throws IOException { if (usePieceValues()) { Obscurable p = pieceClass.getPieceValueMask(); if (p != null) { if (hideState == HideState.INFO_HIDDEN) p.mySetState(getPlayer().getName()); } return p; } else { return null; } } protected UsePrototype getUsePrototype() { return pieceClass.getUsePrototypeDecorator(); } public double getFacingAngle() { if (facing >= FACING_ANGLES.length) return 0.0; else return FACING_ANGLES[facing]; } protected Embellishment getDefendedEmbellishment() throws IOException { Embellishment layer = pieceClass.getDefendedEmbellishmentDecorator(); SequenceEncoder se = new SequenceEncoder(';'); se.append(hasDefended() ? 1 : -1).append(""); layer.mySetState(se.getValue()); return layer; } protected Embellishment getAttackedEmbellishment() throws IOException { Embellishment layer = pieceClass.getAttackedEmbellishmentDecorator(); SequenceEncoder se = new SequenceEncoder(';'); se.append(hasAttacked() ? 1 : -1).append(""); layer.mySetState(se.getValue()); return layer; } // TODO: provide angle phase offset for FreeRotator protected FreeRotator getFreeRotator() { FreeRotator p = pieceClass.getFreeRotatorDecorator(); if (p != null) { // for pieces that point from a corner, this will either be completely wrong // or completely right. If we do nothing, it's guaranteed to be completely wrong p.setAngle(getFacingAngle()); } return p; } protected MovementMarkable getMovementMarkable() throws IOException { MovementMarkable p = pieceClass.getMovementMarkableDecorator(); if (p != null) { p.setMoved(hasMoved()); } return p; } //TODO: add more math functions to MouseOverStackViewer including min(), max(), and mean(). // and antialiased characters in MouseOverStackViewer protected PropertySheet getPiecePropertySheet() { if (piecePS == null) { piecePS = new PropertySheet(); SequenceEncoder se = new SequenceEncoder('~'); SequenceEncoder state = new SequenceEncoder('~'); for(int i = 0; i < pieceValues.length; ++i) { if (pieceValues[i] != null && !pieceValues[i].equals("")) { se.append("0" + pieceValues[i]); Object o = getValue(i); if (o instanceof String) state.append((String) o); else if (o instanceof Integer) state.append(o.toString()); else if (o instanceof Boolean) state.append(o.equals(Boolean.TRUE) ? "yes" : "no"); else state.append(""); } } String definition = se.getValue(); String st = piecePS.myGetState(); if (st == null) { st = state.getValue(); } else if (state.getValue() != null) { st = piecePS.myGetState() + "~" + state.getValue(); } if (definition != null && definition.length() > 0) { se = new SequenceEncoder(';'); // properties se.append(definition); se.append(PIECE_PROPERTIES); // menu name se.append('P'); // key se.append(0); // commit se.append("").append("").append(""); // colour piecePS.mySetType(PropertySheet.ID + se.getValue()); piecePS.mySetState(st); } else { piecePS = null; } } return piecePS; } protected PropertySheet getClassPropertySheet() { if (classPS == null) { classPS = pieceClass.getPropertySheetDecorator(); } return classPS; } protected DynamicProperty getDynamicProperty() { DynamicProperty dp = pieceClass.getDynamicPropertyDecorator(); dp.setInner(gamePiece); // so we can change the state below. dp.setValue(drawOnTopOfOthers() ? "1" : "0"); return dp; } protected GamePiece getBasicPiece() throws IOException { String fileName = pieceClass.getImageName(); if (fileName == null) return null; SequenceEncoder se = new SequenceEncoder(BasicPiece.ID, ';'); se.append("").append("").append(fileName).append(getUniqueClassName()); return new BasicPiece(se.getValue()); } // hasAttacked() and hasDefended() will never be used. public boolean hasAttacked() { return (flags & 0x1) > 0; } public boolean hasDefended() { return !hasAttacked() && (flags & 0x2) > 0; } public boolean hasMoved() { return (flags & 0x4) > 0; } public boolean drawOnTopOfOthers() { return (flags & 0x10) > 0; } public String getUniqueClassName() { return pieceClass.getUniqueName(); } public String getClassName() { return pieceClass.getName(); } protected void setValue(int index, String value) { byte[] b = value.getBytes(); int result = 0; for (int i = 0; i < 4; ++i) result = (result<<8) + (b[i]&0xff); values[index] = result; types[index] = ValueType.TEXT; } protected void setValue(int index, boolean value) { if (value) values[index] = 1; else values[index] = 0; types[index] = ValueType.YESNO; } private int getValueAsInt(int index) { return values[index]; } private String getValueAsString(int index) { byte[] b = new byte[4]; int mask = 0x7f000000; int length = 0; for (int i = 0; i < b.length; ++i) { b[i] = (byte) ((values[index] & mask) >> ((3-i)*8)); if (b[i] < 0x20 || b[i] > 0x7e) break; ++length; mask >>= 8; } return new String(b, 0, length); } private boolean getValueAsBoolean(int index) { return values[index] > 0 ? true : false; } public Object getValue(int index) { if (types[index] == null) return null; switch(types[index]) { case NUMERIC: return getValueAsInt(index); case TEXT: return getValueAsString(index); case YESNO: return getValueAsBoolean(index); default: return null; } } protected void writeToArchive(ListWidget list) throws IOException { GamePiece gp = getGamePiece(); if (gp == null) return; pieceSlot = new PieceSlot(gp); insertComponent(pieceSlot, list); } protected PieceSlot getPieceSlot() { return pieceSlot; } } public enum ValueType { NOT_USED, NUMERIC, TEXT, YESNO, CARD } public enum HideState { NOT_HIDDEN, INFO_HIDDEN, HIDDEN } public static class Player { public static final Player ALL_PLAYERS = new Player("All Players", null, 0); public static final Player NO_PLAYERS = new Player("No Player", null, 0); public static final Player UNKNOWN = new Player("Unknown", null, 0); private static int nPlayers = 0; private final String name; private final SymbolSet.SymbolData hiddenSymbol; private final int hiddenPieceOptions; private final int order; private TreeSet<Player> allies = new TreeSet<Player>(new Comparator<Player>() { public int compare(Player p1, Player p2) { return p1.order - p2.order; } }); public Player(String name, SymbolSet.SymbolData hiddenSymbol, int hiddenPieceOptions) { this.name = name; this.hiddenSymbol = hiddenSymbol; // this.searchRange = searchRange > 50 ? 50 : searchRange; this.hiddenPieceOptions = hiddenPieceOptions; order = nPlayers++; allies.add(this); } public boolean useHiddenPieces() { return (hiddenPieceOptions & 0x1) > 0; } public boolean hiddenWhenPlaced() { return (hiddenPieceOptions & 0x2) > 0; } // in ADC2 hiddenInForcePools and hiddenWhenPlaced are different concepts // as you only get to see a list of piece names when you look at the force // pools and those are hidden if hiddenInForcePools is in effect. // If hiddenWhenPlaced is in effect, all players can see the force pools // but units are hidden when they are placed on the board. // In VASSAL, we make them hidden in the force pool either way. public boolean hiddenInForcePools() { return (hiddenPieceOptions & 0x4) > 0 || hiddenWhenPlaced(); } // TODO: add game master option to players public boolean isGameMaster() { return (hiddenPieceOptions & 0x8) > 0; } public SymbolSet.SymbolData getHiddenSymbol() { return hiddenSymbol; } public String getName() { final StringBuilder sb = new StringBuilder(); for (Player p : allies) { if (sb.length() > 0) sb.append('/'); sb.append(p.name); } return sb.toString(); } public void setAlly(Player player) { allies.add(player); } public boolean isAlly(Player player) { if (allies == null) { return false; } return allies.contains(player); } @Override public String toString() { return getName(); } } private HashMap<Integer,SymbolSet> cardDecks = new HashMap<Integer,SymbolSet>(); public class CardClass extends PieceClass { private final int setIndex; private final int symbolIndex; public CardClass(String name, int symbolIndex, int setIndex) { super(name, null, ALL_PLAYERS, NO_HIDDEN_SYMBOL, 0); this.setIndex = setIndex; this.symbolIndex = symbolIndex; } @Override public String getHiddenName() { if (getOwner() == Player.NO_PLAYERS || getOwner() == Player.ALL_PLAYERS) { return "Unknown card"; } else { return "Unknown " + getOwner().getName() + " card"; } } @Override public SymbolData getHiddenSymbol() throws IOException { return getCardDeck(setIndex).getGamePiece(0); } @Override protected SymbolData getSymbol() throws IOException { if (symbol == null) { SymbolSet set = getCardDeck(setIndex); symbol = set.getGamePiece(symbolIndex); } return symbol; } @Override protected void setValue(int index, boolean value) { assert(false); } @Override protected void setValue(int index, int value) { assert(false); } @Override protected void setValue(int index, String value) { assert(false); } @Override public FreeRotator getFreeRotatorDecorator() { return null; } @Override public Obscurable getPieceValueMask() throws IOException { return null; } @Override public boolean checkHidden(Piece piece) { if (piece.getForcePool() == null) { return piece.hideState == HideState.HIDDEN; } else if (piece.getForcePool() instanceof HandPool) { Player player = getPlayer(piece); return player != Player.ALL_PLAYERS && player != Player.NO_PLAYERS; } else { return false; } } @Override public PropertySheet getPropertySheetDecorator() { return null; } @Override public Obscurable getHiddenDecorator() throws IOException { Obscurable p; SequenceEncoder se = new SequenceEncoder(';'); se.append(new NamedKeyStroke(KeyStroke.getKeyStroke('H', InputEvent.CTRL_MASK))); // key command se.append(getHiddenSymbol().getFileName()); // hide image se.append("Hide Piece"); // menu name BufferedImage image = getSymbol().getImage(); se.append("G" + getFlagLayer(new Dimension(image.getWidth(), image.getHeight()), StateFlag.MARKER)); // display style se.append(getHiddenName()); // mask name if (getOwner() == Player.NO_PLAYERS || getOwner() == Player.ALL_PLAYERS) { se.append("side:"); } else { se.append("sides:" + getOwner().getName()); // owning player } p = new Obscurable(); p.mySetType(Obscurable.ID + se.getValue()); return p; } @Override public Player getOwner() { return Player.ALL_PLAYERS; } @Override public Player getPlayer(Piece p) { if (p.inForcePool() && p.getForcePool() instanceof HandPool) { return ((HandPool) p.getForcePool()).getOwner(); } else { return getOwner(); } } } protected static final int ALL_PLAYERS = 200; protected static final int NO_PLAYERS = 201; /** * A general class for a game piece. Typically all pieces that appear to be identical blong to the * same class. */ public class PieceClass { public PieceClass backReplace; public static final String CLASS_PROPERTIES = "Class Properties"; protected static final int NO_HIDDEN_SYMBOL = 30001; protected static final int PLAYER_DEFAULT_HIDDEN_SYMBOL = 30000; private final int[] values = new int[8]; private final ValueType[] types = new ValueType[8]; private final String name; protected SymbolSet.SymbolData symbol; protected int owner; private final int hiddenSymbol; private final int facing; private PieceClass flipClass; private Piece defaultPiece; private String uniqueName; private boolean flipClassAdded = false; private Piece flipDefinition; public PieceClass(String name, SymbolSet.SymbolData symbol, int owner, int hiddenSymbol, int facing) { this.name = name; this.symbol = symbol; this.owner = owner; this.hiddenSymbol = hiddenSymbol; this.facing = facing; } public Decorator getReplaceWithPreviousDecorator() throws IOException { final PieceClass flipClass = getBackFlipClass(); if (flipClass == null) return null; // don't bother if there are only two counters that flip back and forth else if (getFlipClass() == flipClass) return null; final GameModule gameModule = GameModule.getGameModule(); final String path = flipClass.getFlipClassTreeConfigurePath(); final SequenceEncoder se = new SequenceEncoder(path, ';'); se.append("null").append(0).append(0).append(true).append((NamedKeyStroke) null).append("").append("").append(2).append(true); flipClass.writeFlipDefinition(gameModule); return new Replace(Replace.ID + "Flip Back;B;" + se.getValue(), null); } // TODO: find a different way to do this so that we don't have to generate unique class names. private String getFlipClassTreeConfigurePath() { SequenceEncoder se2 = new SequenceEncoder(PieceWindow.class.getName(), ':'); se2.append(FLIP_DEFINITIONS); final SequenceEncoder se = new SequenceEncoder(se2.getValue(), '/'); se2 = new SequenceEncoder(ListWidget.class.getName(), ':'); se.append(se2.getValue()); se2 = new SequenceEncoder(PieceSlot.class.getName(), ':'); se2.append(getUniqueName()); se.append(se2.getValue()); return se.getValue(); } public Decorator getReplaceWithOtherDecorator() throws IOException { GameModule gameModule = GameModule.getGameModule(); final PieceClass flipClass = getFlipClass(); if (flipClass == null) return null; SequenceEncoder se; String path = flipClass.getFlipClassTreeConfigurePath(); se = new SequenceEncoder(path, ';'); se.append("null").append(0).append(0).append(true).append((NamedKeyStroke) null).append("").append("").append(2).append(true); flipClass.writeFlipDefinition(gameModule); return new Replace(Replace.ID + "Flip;F;" + se.getValue(), null); } private void writeFlipDefinition(GameModule gameModule) throws IOException { if (!flipClassAdded) { flipClassAdded = true; ListWidget list = flipDefs.getAllDescendantComponentsOf(ListWidget.class).iterator().next(); getFlipDefinition().writeToArchive(list); } } public PieceClass getBackFlipClass() { if (backReplace == this) { // this will probably never happen return null; } else { return backReplace; } } public DynamicProperty getDynamicPropertyDecorator() { SequenceEncoder type = new SequenceEncoder(';'); type.append("Layer"); SequenceEncoder constraints = new SequenceEncoder(','); constraints.append(true).append(0).append(1).append(true); type.append(constraints.getValue()); SequenceEncoder command = new SequenceEncoder(':'); KeyStroke stroke = KeyStroke.getKeyStroke('=', InputEvent.SHIFT_DOWN_MASK); SequenceEncoder change = new SequenceEncoder(','); change.append('I').append(1); command.append("Draw on top").append(stroke.getKeyCode() + "," + stroke.getModifiers()).append(change.getValue()); type.append(new SequenceEncoder(command.getValue(), ',').getValue()); DynamicProperty dp = new DynamicProperty(); dp.mySetType(DynamicProperty.ID + type.getValue()); return dp; } // need a unique name for the basic piece so that flip definitions will work public String getUniqueName() { if (uniqueName == null) { uniqueName = getName(); int index = 1; while (uniquePieceNames.contains(uniqueName)) { uniqueName = getName() + " (" + (index++) + ")"; } uniquePieceNames.add(uniqueName); } return uniqueName; } public boolean checkHidden(Piece piece) { return piece.hideState == HideState.HIDDEN || piece.inForcePool() && getOwner().hiddenInForcePools(); } public PropertySheet getPropertySheetDecorator() { SequenceEncoder type = new SequenceEncoder('~'); SequenceEncoder state = new SequenceEncoder('~'); for(int i = 0; i < classValues.length; ++i) { if (classValues[i] != null && !classValues[i].equals("")) { type.append("0" + classValues[i]); Object o = getValue(i); if (o instanceof String) state.append((String) o); else if (o instanceof Integer) state.append(o.toString()); else if (o instanceof Boolean) state.append(o.equals(Boolean.TRUE) ? "yes" : "no"); else state.append(""); } } PropertySheet p = null; if (type.getValue() != null && type.getValue().length() > 0) { p = new PropertySheet(); SequenceEncoder se = new SequenceEncoder(';'); // properties se.append(type.getValue() == null ? "" : type.getValue()); se.append(CLASS_PROPERTIES); // menu name se.append('C'); // key se.append(0); // commit se.append("").append("").append(""); // colour p.mySetType(PropertySheet.ID + se.getValue()); p.mySetState(state.getValue()); } return p; } public UsePrototype getUsePrototypeDecorator() { SequenceEncoder se = new SequenceEncoder(UsePrototype.ID.replaceAll(";", ""), ';'); se.append(COMMON_PROPERTIES); UsePrototype p = new UsePrototype(); p.mySetType(se.getValue()); return p; } public Embellishment getAttackedEmbellishmentDecorator() throws IOException { return getCombatEmbellishmentDecorator("Mark Attacked", "A", StateFlag.ATTACK); } public Embellishment getDefendedEmbellishmentDecorator() throws IOException { return getCombatEmbellishmentDecorator("Mark Defended", "D", StateFlag.DEFEND); } private Embellishment getCombatEmbellishmentDecorator(String command, String key, StateFlag flag) throws IOException { BufferedImage image = getSymbol().getImage(); int xOffset = (image.getWidth()+1)/2 + 5; int yOffset = 0; String imageName = getFlagTab(image.getHeight(), flag); SequenceEncoder se = new SequenceEncoder(';'); se.append(command) // Activate command .append(InputEvent.CTRL_MASK) // Activate modifiers .append(key) // Activate key .append("") // Up command .append(0) // Up modifiers .append("") // Up key .append("") // Down command .append(0) // Down modifiers .append("") // Down key .append("") // Reset command .append("") // Reset key .append("") // Reset level .append(false) // Draw underneath when selected .append(xOffset) // x offset .append(yOffset) // y offset .append(StringArrayConfigurer.arrayToString(new String[] {imageName})) // Image name .append(StringArrayConfigurer.arrayToString(new String[] {""})) .append(false) // loop levels .append(command) // name .append((NamedKeyStroke) null) // Random key .append("") // Random text .append(false) // Follow property .append("") // Property name .append(1); // First level value Embellishment layer = new Embellishment(); layer.mySetType(Embellishment.ID + se.getValue()); return layer; } // TODO: permit offset to mask image. public Obscurable getPieceValueMask() throws IOException { if (getOwner().useHiddenPieces()) { SequenceEncoder se = new SequenceEncoder(';'); se.append(new NamedKeyStroke(KeyStroke.getKeyStroke('I', InputEvent.CTRL_MASK))); // key command se.append(getImageName()); // hide image se.append("Hide Info"); // menu name BufferedImage image = getSymbol().getImage(); se.append("G" + getFlagLayer(new Dimension(image.getWidth(), image.getHeight()), StateFlag.INFO)); // display style if (name == null) se.append(getName()); else se.append("Unknown Piece"); // mask name se.append("sides:" + getOwner().getName()); // owning player Obscurable p = new Obscurable(); p.mySetType(Obscurable.ID + se.getValue()); return p; } else { return null; } } public MovementMarkable getMovementMarkableDecorator() throws IOException { SequenceEncoder se = new SequenceEncoder(';'); BufferedImage img = getSymbol().getImage(); int xOffset = (img.getWidth()+1)/2; int yOffset = -img.getHeight()/2; String movedIcon = getFlagTab(img.getHeight(), StateFlag.MOVE); se.append(movedIcon).append(xOffset).append(yOffset); MovementMarkable p = new MovementMarkable(); p.mySetType(MovementMarkable.ID + se.getValue()); return p; } public Decorator getHiddenDecorator() throws IOException { if (getOwner().useHiddenPieces()) { Decorator p; String sides; if (getOwner() == Player.ALL_PLAYERS || getOwner() == Player.NO_PLAYERS) { sides = "side:"; } else { sides = "sides:" + getOwner().getName(); } SequenceEncoder se = new SequenceEncoder(';'); se.append(new NamedKeyStroke(KeyStroke.getKeyStroke('H', InputEvent.CTRL_MASK))); // key command if (getHiddenSymbol() == null) { // TODO Add transparency to background color as well as alpha for unit. se.append("Hide Piece"); // command se.append(new Color(255, 255, 255)); // background colour se.append(sides); // owning player p = new Hideable(); ((Hideable) p).mySetType(Hideable.ID + se.getValue()); } else { se.append(getHiddenSymbol().getFileName()); // hide image se.append("Hide Piece"); // menu name BufferedImage image = getSymbol().getImage(); se.append("G" + getFlagLayer(new Dimension(image.getWidth(), image.getHeight()), StateFlag.MARKER)); // display style se.append(getHiddenName()); // mask name se.append(sides); // owning player p = new Obscurable(); ((Obscurable) p).mySetType(Obscurable.ID + se.getValue()); } return p; } else { return null; } } public FreeRotator getFreeRotatorDecorator() { int nsides = getMap().getNFaces(); int nfacings; switch (getAllowedFacings()) { case NONE: return null; case FLAT_SIDES: nfacings = nsides == 4 ? 4 : 12; break; default: nfacings = nsides == 4 ? 8 : 24; } String type = FreeRotator.ID + nfacings + ";];[;Rotate CW;Rotate CCW;;;;"; FreeRotator p = new FreeRotator(); p.mySetType(type); return p; } public String getHiddenName() { return "Unknown Piece"; } protected void setValue(int index, int value) { values[index] = value; types[index] = ValueType.NUMERIC; } public FacingDirection getAllowedFacings() { if (allowedFacings == null) return FacingDirection.NONE; else if (facing >= allowedFacings.length) return FacingDirection.NONE; else return allowedFacings[facing]; } public SymbolSet.SymbolData getHiddenSymbol() throws IOException { if (hiddenSymbol == PLAYER_DEFAULT_HIDDEN_SYMBOL) return getOwner().getHiddenSymbol(); else if (hiddenSymbol == NO_HIDDEN_SYMBOL) return null; else return getSet().getGamePiece(hiddenSymbol); } public String getImageName() throws IOException { if (getSymbol() == null) return null; else return symbol.getFileName(); } public Player getOwner() { if (owner == NO_PLAYERS) return Player.NO_PLAYERS; else if (owner >= players.size()) return Player.ALL_PLAYERS; else return players.get(owner); } public Player getPlayer(Piece p) { return getOwner(); } protected void setFlipClass(int to) { if (to >= 0 && to < pieceClasses.size()) flipClass = pieceClasses.get(to); } protected void setBackFlipClass(int from) { backReplace = pieceClasses.get(from); assert(backReplace.getFlipClass() == this); } public PieceClass getFlipClass() { if (flipClass == this) { // if the flip class is this, then it doesn't count return null; } else { return flipClass; } } public String getName() { return name; } protected void setValue(int index, String value) { byte[] b = value.getBytes(); int result = 0; for (int i = 0; i < 4; ++i) result = (result<<8) + (b[i]&0xff); values[index] = result; types[index] = ValueType.TEXT; } protected void setValue(int index, boolean value) { if (value) values[index] = 1; else values[index] = 0; types[index] = ValueType.YESNO; } public int getValueAsInt(int index) { return values[index]; } public String getValueAsString(int index) { byte[] b = new byte[4]; int length = 0; int mask = 0x7f000000; for (int i = 0; i < b.length; ++i) { b[i] = (byte) ((values[index] & mask) >> ((3-i)*8)); if (b[i] < 0x20 || b[i] > 0x7e) break; ++length; mask >>= 8; } return new String(b, 0, length); } public int getNValues() { int total = 0; for (ValueType t : types) if (t != ValueType.NOT_USED) ++total; return total; } public boolean getValueAsBoolean(int index) { return values[index] > 0 ? true : false; } public Object getValue(int index) { if (types[index] == null) return null; switch(types[index]) { case NUMERIC: return getValueAsInt(index); case TEXT: return getValueAsString(index); case YESNO: return getValueAsBoolean(index); default: return null; } } protected void writeToArchive(ListWidget list) throws IOException { getDefaultPiece().writeToArchive(list); } protected Piece getDefaultPiece() { if (defaultPiece == null) defaultPiece = new Piece(this); return defaultPiece; } protected Piece getFlipDefinition() { if (flipDefinition == null) { flipDefinition = new Piece(this); } return flipDefinition; } protected SymbolSet.SymbolData getSymbol() throws IOException { return symbol; } } public static final String COMMON_PROPERTIES = "Common Properties"; private String name; private MapBoard map = null; @SuppressWarnings("unused") private int gameTurn = -1; private final ArrayList<PieceClass> pieceClasses = new ArrayList<PieceClass>(); private final ArrayList<Piece> pieces = new ArrayList<Piece>(); private final ArrayList<Player> players = new ArrayList<Player>(); private final HashMap<Integer,ArrayList<Piece>> stacks = new HashMap<Integer,ArrayList<Piece>>(); private final HashMap<Integer, ArrayList<Piece>> forcePoolHashMap = new HashMap<Integer,ArrayList<Piece>>(); private ForcePoolList forcePools = new ForcePoolList(); private final String[] classValues = new String[8]; private final String[] pieceValues = new String[8]; private FacingDirection allowedFacings[]; protected PieceClass getClassFromIndex(int index) { if (index < 0 || index >= pieceClasses.size()) return null; return pieceClasses.get(index); } protected SymbolSet getCardDeck(int deck) throws IOException { SymbolSet set = cardDecks.get(deck); if (set == null) { File f = action.getCaseInsensitiveFile(new File(deckName + "-c" + (deck+1) + ".set"), file, true, new ExtensionFileFilter(ADC2Utils.SET_DESCRIPTION, new String[] {ADC2Utils.SET_EXTENSION})); if (f == null) throw new FileNotFoundException("Unable to locate deck symbol set."); set = new SymbolSet(); set.importCardSet(action, f); cardDecks.put(deck, set); } return set; } private HashMap<StateFlag, HashMap<Dimension, String>> hiddenFlagImages; private int version; private int classCombatSummaryValues; private int pieceCombatSummaryValues; private final StatusDots[] statusDots = new StatusDots[6]; private final ArrayList<String> turnNames = new ArrayList<String>(); private boolean useLOS; private String deckName; private int nCardSets; private final String infoPages[] = new String[10]; private String infoPageName; public static final Color FLAG_BACKGROUND = new Color(1.0f, 1.0f, 0.8f, 0.8f); public static final Color FLAG_FOREGROUND = new Color(0.5f, 0.0f, 0.5f, 1.0f); // public static final Color FLAG_BACKGROUND = Color.BLACK; // public static final Color FLAG_FOREGROUND = Color.WHITE; private int nFlipDefs = 0; private PieceWindow flipDefs; private PieceWindow pieceWin; public static class StateFlag { public static final StateFlag MOVE = new StateFlag("M", FLAG_BACKGROUND, FLAG_FOREGROUND, 0); public static final StateFlag ATTACK = new StateFlag("A", FLAG_BACKGROUND, FLAG_FOREGROUND, 1); public static final StateFlag DEFEND = new StateFlag("D", FLAG_BACKGROUND, FLAG_FOREGROUND, 1); public static final StateFlag INFO = new StateFlag("h", FLAG_BACKGROUND, FLAG_FOREGROUND, 2); public static final StateFlag MARKER = new StateFlag("H", FLAG_BACKGROUND, FLAG_FOREGROUND, 2); public static final StateFlag COMBAT = new StateFlag("C", FLAG_BACKGROUND, FLAG_FOREGROUND, 1); private final String name; private final Color background; private final Color foreground; private final int tab; private String imageName; private final ArrayList<StatusDots> statusDots = new ArrayList<StatusDots>(); public StateFlag(String flag, Color background, Color foreground, int tab) { this.name = flag; this.background = background; this.foreground = foreground; this.tab = tab; } public String getStatusIconName() throws IOException { if (imageName == null) { final BufferedImage icon = new BufferedImage(10, 15, BufferedImage.TYPE_INT_ARGB); Graphics2D g = icon.createGraphics(); drawFlagImage(g); imageName = getUniqueImageFileName(name, ".png"); ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(icon, "png", out); byte[] imageDataArray = out.toByteArray(); GameModule.getGameModule().getArchiveWriter().addImage(imageName, imageDataArray); } return imageName; } public void addStatusDots(StatusDots dots) { statusDots.add(dots); } public void drawFlagImage(Graphics2D g) { final int tabHeight = 15; final int tabWidth = 10; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setColor(background); g.fillRoundRect(-tabWidth, 0, 2*tabWidth, tabHeight, 6, 6); g.setColor(foreground); g.drawRoundRect(-tabWidth, 0, 2*tabWidth-1, tabHeight-1, 6, 6); g.setFont(new Font("Dialog", Font.PLAIN, 9)); Rectangle2D r = g.getFontMetrics().getStringBounds(name, g); g.drawString(name, tabWidth/2 - (int) (r.getWidth()/2.0) - 1, 11); g.setBackground(new Color(0,0,0,0)); g.clearRect(-tabWidth, 0, tabWidth, tabHeight); } } private String getFlagTab(int height, StateFlag flag) throws IOException { if (hiddenFlagImages == null) hiddenFlagImages = new HashMap<StateFlag, HashMap<Dimension,String>>(); HashMap<Dimension,String> map = hiddenFlagImages.get(flag); if (map == null) { map = new HashMap<Dimension,String>(); hiddenFlagImages.put(flag, map); } Dimension d = new Dimension(0, height); String imageName = map.get(d); if (imageName == null) { int tabHeight = 15; int tabSpace = height < 43 ? (height-tabHeight)/2 : tabHeight-1; int tabWidth = 10; final BufferedImage icon = new BufferedImage(tabWidth, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = icon.createGraphics(); g.translate(0, tabSpace*flag.tab); flag.drawFlagImage(g); imageName = getUniqueImageFileName(flag.name + 0 + "x" + height); map.put(d, imageName); ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(icon, "png", out); byte[] imageDataArray = out.toByteArray(); GameModule.getGameModule().getArchiveWriter().addImage(imageName, imageDataArray); } return imageName; } private String getFlagLayer(Dimension d, StateFlag flag) throws IOException { if (hiddenFlagImages == null) hiddenFlagImages = new HashMap<StateFlag, HashMap<Dimension,String>>(); HashMap<Dimension,String> map = hiddenFlagImages.get(flag); if (map == null) { map = new HashMap<Dimension,String>(); hiddenFlagImages.put(flag, map); } String imageName = map.get(d); if (imageName == null) { int tabHeight = 15; int tabSpace = d.height < 43 ? (d.height-tabHeight)/2 : tabHeight-1; int tabWidth = 10; final BufferedImage icon = new BufferedImage( d.width + 2*tabWidth, d.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = icon.createGraphics(); g.translate(d.width+tabWidth, tabSpace*flag.tab); flag.drawFlagImage(g); imageName = getUniqueImageFileName(flag.name + d.width + "x" + d.height); map.put(d, imageName); ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(icon, "png", out); byte[] imageDataArray = out.toByteArray(); GameModule.getGameModule().getArchiveWriter().addImage(imageName, imageDataArray); } return imageName; } public boolean usePieceValues() { for (int i = 0; i < pieceValues.length; ++i) { if (pieceValues[i] != null && !pieceValues[i].equals("")) return true; } return false; } @Override protected void load(File f) throws IOException { super.load(f); DataInputStream in = null; try { in = new DataInputStream(new BufferedInputStream(new FileInputStream(f))); name = stripExtension(f.getName()); int header = in.readByte(); if (header != -3 && header != -2) throw new FileFormatException("Invalid Game Module Header"); // TODO: figure out version-specific formats for older versions. version = in.readUnsignedShort(); String s = readWindowsFileName(in); String mapFileName = forceExtension(s, "map"); map = new MapBoard(); File mapFile = action.getCaseInsensitiveFile(new File(mapFileName), f, true, new ExtensionFileFilter(ADC2Utils.MAP_DESCRIPTION, new String[] {ADC2Utils.MAP_EXTENSION})); if (mapFile == null) throw new FileNotFoundException("Unable to locate map file."); map.importFile(action, mapFile); // TODO: each block has an ideosyncratic way of terminating itself. // this has to be tested extensively. try { readGameTurnBlock(in); readClassBlock(in); readClassValueBlock(in); readPieceBlock(in); readPieceValueBlock(in); readPlayerBlock(in); readReplayBlock(in); readPoolBlock(in); readStackBlock(in); readCombatSummaryBlock(in); readFacingBlock(in); readSoundSettingBlock(in); readFlipDefinitionBlock(in); readPieceStatusDotsBlock(in); readDiceBlock(in); readTurnNameBlock(in); readLOSBlock(in); readLOSFlagBlock(in); readDeckNameBlock(in); readPoolOwnerBlock(in); readAutoRevealWhenMovingLOSFlagBlock(in); readCombatRevealFlagBlock(in); readInfoPageBlock(in); readInfoSizeBlock(in); readAllianceBlock(in); readDrawOptionsBlock(in); readPieceStatusDotsBlock(in); // read this in again! } catch(ADC2Utils.NoMoreBlocksException e) { } in.close(); } finally { IOUtils.closeQuietly(in); } } // TODO: what happens when this conflicts with the draw options in the map file itself? private void readDrawOptionsBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Draw Options"); @SuppressWarnings("unused") boolean showHexSides = in.readByte() != 0; @SuppressWarnings("unused") boolean showHexLines = in.readByte() != 0; @SuppressWarnings("unused") boolean showPlaceNames = in.readByte() != 0; int pieceOptionFlags = in.readUnsignedByte(); @SuppressWarnings("unused") boolean showPieces = (pieceOptionFlags & 0x1) > 0; @SuppressWarnings("unused") boolean showMarkers = (pieceOptionFlags & 0x2) == 0; /* * First three bytes give symbols per hex for the three zoomlevels. * 1 = 1 (inside hex) * 4 = 4 per hex * 101 = 1 (inside hex) & overlay stack on pieces * 104 = 4 per hex & overlay stack on pieces * 200 = 1 (centered) * 201 = 1 (centered) & overlay stack on pieces * all other values are completely invalid. * * The purpose of the last byte in this block is unknown. */ in.readFully(new byte[4]); } // TODO: allow multiple players to see hidden units. protected void readAllianceBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Alliances"); /* int nAlliances = */ ADC2Utils.readBase250Word(in); // ignored for (int i = 0; i < players.size(); ++i) { in.readUnsignedShort(); // unknown for (int j = 0; j < players.size(); ++j) { if (in.readUnsignedShort() > 0) { players.get(i).setAlly(players.get(j)); } } } } protected void readInfoSizeBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Info Size"); in.readFully(new byte[4]); } protected void readInfoPageBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Info Page"); infoPageName = readWindowsFileName(in); if (infoPageName.length() > 0) { File ipx = action.getCaseInsensitiveFile(new File(forceExtension(infoPageName, "ipx")), file, true, new ExtensionFileFilter("Info page file (*.ipx;*.IPX)", new String[] {".ipx"})); if (ipx != null) { DataInputStream input = null; try { input = new DataInputStream(new BufferedInputStream(new FileInputStream(ipx))); try { while (true) { // loop until EOF while (input.readUnsignedByte() != 0x3b) { } int idx = input.readUnsignedByte(); int len = input.readUnsignedByte(); byte dimensions[] = new byte[8]; input.readFully(dimensions); byte buf[] = new byte[len]; input.readFully(buf); String name = new String(buf); if (idx >=0 && idx <10 && infoPages[idx] == null) { infoPages[idx] = name; } } } catch (EOFException e) { // do nothing } input.close(); } finally { IOUtils.closeQuietly(input); } } else { infoPageName = null; } } } protected void readCombatRevealFlagBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Combat Reveal Option Flag"); in.readByte(); } protected void readAutoRevealWhenMovingLOSFlagBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Auto Reveal When Moving (LOS) Flag"); in.readByte(); } protected void readPoolOwnerBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Pool Owner"); Iterator<Pool> iter = forcePools.iterator(); for (int i = 0; i < forcePools.size(); ++i) { Pool p = iter.next(); int owner = in.readUnsignedByte(); if (p instanceof Cards) { ((Cards) p).setOwner(owner); } } } protected void readDeckNameBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Deck Name"); deckName = stripExtension(readWindowsFileName(in)); } protected void readLOSFlagBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "LOS Flags"); /* boolean useElevation = */ in.readByte(); int rightMouseButton = in.readByte(); /* 0 = normal 1 = move pieces 2 = zoom in out 3 = single LOS 4 = area LOS 5 = redraw map 6 = place pieces ? */ if (rightMouseButton == 3 || rightMouseButton == 4) useLOS = true; } protected void readLOSBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "LOS"); in.readFully(new byte[18]); // unknown /* int maxRange = */ in.readUnsignedShort(); /* int degradeLOS = */ in.readByte(); /* int pointsToBlockLOS = */ ADC2Utils.readBase250Word(in); /* String levelHeight = */ readNullTerminatedString(in, 20); /* int useSmoothing = */ in.readByte(); /* int cliffElevation = */ ADC2Utils.readBase250Word(in); if (version > 0x0206) { /* int hexSize = */ ADC2Utils.readBase250Word(in); byte[] units = new byte[10]; in.readFully(units); } int nBlocks = ADC2Utils.readBase250Word(in); for (int i = 0; i < nBlocks; ++i) { /* int blockPoints = */ ADC2Utils.readBase250Word(in); /* int baseElevation = */ ADC2Utils.readBase250Word(in); /* int aboveGroundLevel = */ ADC2Utils.readBase250Word(in); /* int whenSpotting = */ ADC2Utils.readBase250Word(in); /* int whenTarget = */ ADC2Utils.readBase250Word(in); byte[] color = new byte[3]; in.readFully(color); } } protected void readTurnNameBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Turn Names"); int nNames = ADC2Utils.readBase250Word(in); boolean terminate = false; for (int i = 0; i < nNames; ++i) { String name = readNullTerminatedString(in, 50); if (name.equals("")) terminate = true; else if (!terminate) turnNames.add(name); } } // makers of ADC2 modules never seem to make use of this. protected void readDiceBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Dice"); /* boolean autoRevealWhenMoving = */ in.readByte(); in.readByte(); // unknown /* int ndice = */ in.readUnsignedByte(); /* int nsides = */ in.readUnsignedByte(); } // All of this information appears to be ignored by ADC2. This information // is read again later in the file. protected void readPieceStatusDotsBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Piece Status Dots"); byte[] size = new byte[3]; for (int i = 0; i < 6; ++i) { int type = in.readByte(); int show = ADC2Utils.readBase250Word(in); int color = in.readUnsignedByte(); int position = in.readByte(); in.readFully(size); statusDots[i] = new StatusDots(type, show, ADC2Utils.getColorFromIndex(color), position, size[2]); } } protected void readFlipDefinitionBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Flip Definition"); in.readUnsignedByte(); // unknown byte nFlipDefs = ADC2Utils.readBase250Word(in); for (int i = 0; i < nFlipDefs; ++i) { int from = ADC2Utils.readBase250Word(in); int to = ADC2Utils.readBase250Word(in); if (from >= 0 && from < pieceClasses.size() && to >= 0 && to < pieceClasses.size()) { pieceClasses.get(from).setFlipClass(to); pieceClasses.get(to).setBackFlipClass(from); } } } protected void readSoundSettingBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Sound Settings"); for (int i = 0; i < 3; ++i) /* scrollJumpSize[i] = */ in.readUnsignedByte(); in.readFully(new byte[3]); // unknown /* soundOn = */ in.readUnsignedByte(); } public enum FacingDirection { FLAT_SIDES, VERTEX, BOTH, NONE } protected void readFacingBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Facing"); final int nFacing = in.readUnsignedByte(); allowedFacings = new FacingDirection[nFacing+1]; allowedFacings[0] = FacingDirection.NONE; for (int i = 0; i < nFacing; ++i) { /* String styleName = */ readNullTerminatedString(in); int direction = in.readUnsignedByte(); // one invalid facing struct will invalidate all later ones. if (i == 0 || allowedFacings[i] != FacingDirection.NONE) { switch (direction) { case 2: allowedFacings[i+1] = FacingDirection.VERTEX; break; case 3: allowedFacings[i+1] = FacingDirection.BOTH; break; default: allowedFacings[i+1] = FacingDirection.FLAT_SIDES; } } else { allowedFacings[i+1] = FacingDirection.NONE; } // this describes how the arrow is drawn in ADC2 /* int display = */ in.readUnsignedByte(); /* int fillColor = */ in.readUnsignedByte(); /* int outlineColor = */ in.readUnsignedByte(); // zoom sizes in.readFully(new byte[3]); } } protected void readCombatSummaryBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Fast Zoom"); /* int fastZoom = */ in.readUnsignedByte(); classCombatSummaryValues = in.readUnsignedByte(); pieceCombatSummaryValues = in.readUnsignedByte(); /* int fastDraw = */ in.readUnsignedByte(); } // None of this is either doable or appropriate in VASSAL. protected void readStackBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Stack"); final int nStackDefs = ADC2Utils.readBase250Word(in); for (int i = 0; i < nStackDefs; ++i) { // SymbolSet.SymbolData symbol = getSet().getGamePiece( ADC2Utils.readBase250Word(in); // ); // int mustContain = ADC2Utils.readBase250Word(in); // class index for (int j = 0; j < 3; ++j) {// one per zoom level // int atLeastNPieces = in.readUnsignedByte(); } // int owningPlayer = ADC2Utils.readBase250Word(in); } } // TODO: this is a big job to implement and may not even be worth doing at all. protected void readReplayBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Replay"); int nBytes; if (version > 0x0203) nBytes = ADC2Utils.readBase250Integer(in); else nBytes = ADC2Utils.readBase250Word(in); in.readFully(new byte[nBytes]); } protected void readPoolBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, FORCE_POOL); final int nForcePools = ADC2Utils.readBase250Word(in); for (int i = 0; i < nForcePools; ++i) { String n = readNullTerminatedString(in, 25); int type = in.readByte(); in.readFully(new byte[2]); // not sure what these do int nunits = ADC2Utils.readBase250Word(in); // ignored if (nunits != FORCE_POOL_BLOCK_END) { switch (type) { case 2: forcePools.add(new HandPool(n, forcePoolHashMap.get(i))); break; case 3: forcePools.add(new DeckPool(n, forcePoolHashMap.get(i))); break; default: forcePools.add(new ForcePool(n, forcePoolHashMap.get(i))); } } else break; } } protected void readPlayerBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Player"); // file format doesn't actually care how many players it says there is. /* int nplayers = */ ADC2Utils.readBase250Word(in); // The last player is typically an empty string which indicates the end of // the list despite the fact that the number of players is clearly // indicated. String name; do { name = readNullTerminatedString(in, 25); // someday we may try to crack this, but since it doesn't actually encrypt anything // there's no real point. // byte[] password = new byte[20]; in.readFully(new byte[20]); /* int startingZoomLevel = */ in.readByte(); /* int startingPosition = */ ADC2Utils.readBase250Word(in); SymbolSet.SymbolData hiddenSymbol = getSet().getGamePiece(ADC2Utils.readBase250Word(in)); /* String picture = */ readNullTerminatedString(in); // we don't do anything with this. /* int searchRange = */ in.readUnsignedByte(); int hiddenPieceOptions = in.readUnsignedByte(); in.readByte(); // padding if (name.length() > 0) { Player player = new Player(name, hiddenSymbol, hiddenPieceOptions); players.add(player); } } while (name.length() > 0); } @SuppressWarnings("fallthrough") protected void readPieceBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, PIECE); int nPieces = ADC2Utils.readBase250Word(in); for (int i = 0; i < nPieces; ++i) { String name = readNullTerminatedString(in, 25); PieceClass cl = getClassFromIndex(ADC2Utils.readBase250Word(in)); if (cl == null) throw new FileFormatException("Invalid Class Index"); // prevent duplication if (name.equals(cl.getName())) name = ""; int values[] = new int[8]; for (int j = 0; j < values.length; ++j) values[j] = in.readInt(); ValueType types[] = new ValueType[8]; for (int j = 0; j < types.length; ++j) { switch (in.readUnsignedByte()) { case 1: types[j] = ValueType.NUMERIC; break; case 2: types[j] = ValueType.TEXT; break; case 3: types[j] = ValueType.YESNO; break; case 10: if (j == 0) { types[j] = ValueType.CARD; break; } // else fall through default: types[j] = ValueType.NOT_USED; break; } } HideState hidden; switch(in.readUnsignedByte()) { case 0: hidden = HideState.NOT_HIDDEN; break; case 1: hidden = HideState.INFO_HIDDEN; break; default: hidden = HideState.HIDDEN; break; } in.readFully(new byte[2]); // don't know what these do int position = ADC2Utils.readBase250Word(in); int flags = in.readUnsignedByte(); int facing = in.readUnsignedByte(); if (facing > FACING_ANGLES.length) facing = 0; Piece p = new Piece(position, name, cl, hidden, flags, facing); for (int j = 0; j < values.length; ++j) { p.setValue(j, values[j]); p.types[j] = types[j]; } pieces.add(p); } } protected void readClassValueBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Class Value"); for (int i = 0; i < classValues.length; ++i) classValues[i] = readNullTerminatedString(in, 15); } protected void readPieceValueBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Piece Value"); for (int i = 0; i < pieceValues .length; ++i) pieceValues[i] = readNullTerminatedString(in, 15); } protected void readClassBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Class"); int nClasses = ADC2Utils.readBase250Word(in); for (int i = 0; i < nClasses; ++i) { int symbolIndex = ADC2Utils.readBase250Word(in); String name = readNullTerminatedString(in, 25); int[] values = new int[8]; for (int j = 0; j < values.length; ++j) values[j] = in.readInt(); boolean isCard = false; int setIndex = 0; ValueType[] types = new ValueType[8]; for (int j = 0; j < types.length; ++j) { int t = in.readUnsignedByte(); if (j == 0 && t == 10) isCard = true; if (isCard) { if (j == 1) setIndex = t; if (setIndex >= nCardSets) nCardSets = setIndex+1; } else { switch (t) { case 1: types[j] = ValueType.NUMERIC; break; case 2: types[j] = ValueType.TEXT; break; case 3: types[j] = ValueType.YESNO; break; default: types[j] = ValueType.NOT_USED; } } } int owner = in.readUnsignedByte(); int hiddenSymbol = ADC2Utils.readBase250Word(in); // 0 = not used. int facing = in.readUnsignedByte(); PieceClass cl; if (isCard) { cl = new CardClass(name, symbolIndex, setIndex); } else { cl = new PieceClass(name, getSet().getGamePiece(symbolIndex), owner, hiddenSymbol, facing); for (int j = 0; j < values.length; ++j) { cl.setValue(j, values[j]); cl.types[j] = types[j]; } } pieceClasses.add(cl); } } protected void readGameTurnBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Game Turn"); gameTurn = ADC2Utils.readBase250Word(in); } protected void writePrototypesToArchive(GameModule gameModule) { PrototypesContainer container = gameModule.getAllDescendantComponentsOf(PrototypesContainer.class).iterator().next(); PrototypeDefinition def = new PrototypeDefinition(); insertComponent(def, container); def.setConfigureName(COMMON_PROPERTIES); // set common properties GamePiece gp = new BasicPiece(); Delete del = new Delete(); SequenceEncoder se = new SequenceEncoder(';'); se.append("Delete").append(new NamedKeyStroke(KeyStroke.getKeyStroke("DELETE"))); del.mySetType(Delete.ID + se.getValue()); del.setInner(gp); gp = del; if (forcePools.count(ForcePool.class) > 0) gp = new ReturnToDeck(ReturnToDeck.ID + "Return to Force Pool;R;;Select Force Pool", gp); se = new SequenceEncoder(';'); se.append(new NamedKeyStroke(KeyStroke.getKeyStroke('T', InputEvent.CTRL_MASK))) .append("Movement Trail") .append(false) .append(false) .append(10) .append(Color.WHITE) .append(Color.BLACK) .append(100) .append(0); gp = new Footprint(Footprint.ID + se.getValue(), gp); se = new SequenceEncoder(','); se.append(ADC2Utils.TYPE); gp = new Marker(Marker.ID + se.getValue(), gp); gp.setProperty(ADC2Utils.TYPE, PIECE); def.setPiece(gp); } @Override public void writeToArchive() throws IOException { GameModule gameModule = GameModule.getGameModule(); gameModule.setAttribute(GameModule.MODULE_NAME, name); writePrototypesToArchive(gameModule); getMap().writeToArchive(); configureStatusFlagButtons(); configureMapLayers(); pieceWin = gameModule.getAllDescendantComponentsOf(PieceWindow.class).iterator().next(); configureFlipDefinitions(gameModule); writeClassesToArchive(gameModule); writeForcePoolsToArchive(gameModule); writeDecksToArchive(gameModule); writeHandsToArchive(gameModule); writeInfoPagesToArchive(gameModule); writeToolbarMenuToArchive(gameModule); writeSetupStacksToArchive(gameModule); writePlayersToArchive(gameModule); configureMouseOverStackViewer(gameModule); configureMainMap(gameModule); configureDiceRoller(gameModule); if (turnNames.size() > 1) // must have at least two turns configureTurnCounter(gameModule); if (useLOS) insertComponent(new LOS_Thread(), gameModule); } private void configureFlipDefinitions(GameModule gameModule) { if (nFlipDefs > 0) { flipDefs = new PieceWindow(); insertComponent(flipDefs, gameModule); flipDefs.setAttribute(PieceWindow.NAME, FLIP_DEFINITIONS); flipDefs.setAttribute(PieceWindow.HIDDEN, Boolean.TRUE); flipDefs.setAttribute(PieceWindow.BUTTON_TEXT, ""); flipDefs.setAttribute(PieceWindow.TOOLTIP, ""); ListWidget list = new ListWidget(); insertComponent(list, flipDefs); } } private void configureMainMap(GameModule gameModule) throws IOException { final Map mainMap = getMainMap(); mainMap.setAttribute(Map.MARK_UNMOVED_ICON, StateFlag.MOVE.getStatusIconName()); // if (usePieceNames) { // mainMap.setAttribute(Map.MOVE_WITHIN_FORMAT, "$" + Map.PIECE_NAME + "$" + "/" + PC_NAME + " moves $" + Map.OLD_LOCATION + "$ -> $" + Map.LOCATION + "$ *"); // mainMap.setAttribute(Map.MOVE_TO_FORMAT, "$" + Map.PIECE_NAME + "$" + "/" + PC_NAME + " moves $" + Map.OLD_LOCATION + "$ -> $" + Map.LOCATION + "$ *"); // mainMap.setAttribute(Map.CREATE_FORMAT, "$" + Map.PIECE_NAME + "$/" + PC_NAME + " created in $" + Map.LOCATION + "$"); // } } private void configureStatusFlagButtons() throws IOException { String imageName; MassKeyCommand command; imageName = StateFlag.ATTACK.getStatusIconName(); command = new MassKeyCommand(); insertComponent(command, getMainMap()); command.setAttribute(MassKeyCommand.TOOLTIP, "Clear attacked status"); command.setAttribute(MassKeyCommand.BUTTON_TEXT, "Attacked"); command.setAttribute(MassKeyCommand.HOTKEY, null); command.setAttribute(MassKeyCommand.ICON, imageName); command.setAttribute(MassKeyCommand.NAME, "Attacked"); command.setAttribute(MassKeyCommand.KEY_COMMAND, new NamedKeyStroke(KeyStroke.getKeyStroke('A', InputEvent.CTRL_DOWN_MASK))); command.setAttribute(MassKeyCommand.PROPERTIES_FILTER, "Mark Attacked_Active = true"); command.setAttribute(MassKeyCommand.DECK_COUNT, -1); command.setAttribute(MassKeyCommand.REPORT_SINGLE, Boolean.TRUE); command.setAttribute(MassKeyCommand.REPORT_FORMAT, ""); imageName = StateFlag.DEFEND.getStatusIconName(); command = new MassKeyCommand(); insertComponent(command, getMainMap()); command.setAttribute(MassKeyCommand.TOOLTIP, "Clear defended status"); command.setAttribute(MassKeyCommand.BUTTON_TEXT, "Defended"); command.setAttribute(MassKeyCommand.HOTKEY, null); command.setAttribute(MassKeyCommand.ICON, imageName); command.setAttribute(MassKeyCommand.NAME, "Defended"); command.setAttribute(MassKeyCommand.KEY_COMMAND, new NamedKeyStroke(KeyStroke.getKeyStroke('D', InputEvent.CTRL_DOWN_MASK))); command.setAttribute(MassKeyCommand.PROPERTIES_FILTER, "Mark Defended_Active = true"); command.setAttribute(MassKeyCommand.DECK_COUNT, -1); command.setAttribute(MassKeyCommand.REPORT_SINGLE, Boolean.TRUE); command.setAttribute(MassKeyCommand.REPORT_FORMAT, ""); MultiActionButton button = new MultiActionButton(); insertComponent(button, getMainMap()); button.setAttribute(MultiActionButton.BUTTON_TEXT, ""); button.setAttribute(MultiActionButton.TOOLTIP, "Clear combat status flags."); button.setAttribute(MultiActionButton.BUTTON_ICON, StateFlag.COMBAT.getStatusIconName()); button.setAttribute(MultiActionButton.BUTTON_HOTKEY, KeyStroke.getKeyStroke('C', InputEvent.CTRL_DOWN_MASK)); button.setAttribute(MultiActionButton.MENU_ITEMS, StringArrayConfigurer.arrayToString(new String[] {"Attacked", "Defended"})); } protected void writeInfoPagesToArchive(GameModule gameModule) throws IOException { if (infoPageName != null && !infoPageName.equals("")) { ChartWindow charts = new ChartWindow(); insertComponent(charts, gameModule); charts.setAttribute(ChartWindow.NAME, CHARTS); charts.setAttribute(ChartWindow.BUTTON_TEXT, CHARTS); charts.setAttribute(ChartWindow.TOOLTIP, CHARTS); charts.setAttribute(ChartWindow.HOTKEY, new NamedKeyStroke(KeyStroke.getKeyStroke('C', InputEvent.CTRL_DOWN_MASK))); TabWidget tab = new TabWidget(); insertComponent(tab, charts); for (int i = 0; i < infoPages.length; ++i) { File f = action.getCaseInsensitiveFile(new File(forceExtension(infoPageName, "b"+i)), file, false, null); if (f == null) { f = action.getCaseInsensitiveFile(new File(forceExtension(infoPageName, "t"+i)), file, false, null); } if (f != null) { Boolean isChart = Character.toLowerCase(getExtension(f.getName()).charAt(0)) == 'b'; Widget w; if (isChart) { w = new Chart(); insertComponent(w, tab); w.setAttribute(Chart.NAME, infoPages[i]); gameModule.getArchiveWriter().addImage(f.getPath(), f.getName()); w.setAttribute(Chart.FILE, f); } else { w = new HtmlChart(); insertComponent(w, tab); w.setAttribute(HtmlChart.NAME, infoPages[i]); StringBuilder sb = new StringBuilder(); sb.append("<html><body>"); BufferedReader input = null; try { input = new BufferedReader(new FileReader(f)); String line = null; do { line = input.readLine(); if (line != null && line.length() > 0) { line = line.replaceAll(" (?: )", " "); line = line.replaceAll("(?<= ) ", " "); line = line.replaceFirst("^ ", " "); sb.append("<p>" + line + "</p>"); } } while (line != null); sb.append("</body></html>"); gameModule.getArchiveWriter().addFile(f.getName(), sb.toString().getBytes()); w.setAttribute(HtmlChart.FILE, f.getName()); input.close(); } finally { IOUtils.closeQuietly(input); } } tab.propertyChange(new PropertyChangeEvent(w, Configurable.NAME_PROPERTY, "", infoPages[i])); } } } } protected void configureMapLayers() { // add game piece layers to map LayeredPieceCollection layer = getLayeredPieceCollection(); String order = layer.getAttributeValueString(LayeredPieceCollection.LAYER_ORDER); if (order.equals("")) { order = "0,1"; } else { order = order + ",0,1"; } layer.setAttribute(LayeredPieceCollection.LAYER_ORDER, order); } protected void configureTurnCounter(GameModule gameModule) { TurnTracker tracker = new TurnTracker(); insertComponent(tracker, gameModule); tracker.setAttribute(TurnTracker.TURN_FORMAT, "$level1$"); ListTurnLevel list = new ListTurnLevel(); insertComponent(list, tracker); list.setAttribute("property", "currentTurn"); String[] names = new String[turnNames.size()]; list.setAttribute("list", StringArrayConfigurer.arrayToString(turnNames.toArray(names))); // TODO: set current turn } protected void configureDiceRoller(GameModule gameModule) { DiceButton dice = new DiceButton(); insertComponent(dice, gameModule); dice.setAttribute(DiceButton.NAME, "Roll"); dice.setAttribute(DiceButton.PROMPT_ALWAYS, Boolean.TRUE); dice.setAttribute(DiceButton.TOOLTIP, "Roll the dice"); dice.setAttribute(DiceButton.BUTTON_TEXT, "Roll"); dice.setAttribute(DiceButton.REPORT_FORMAT, "** $name$ $nDice$d$nSides$ (+$plus$ each) = $result$ *** <$playerName$>"); } protected void configureMouseOverStackViewer(GameModule gameModule) { CounterDetailViewer viewer = gameModule.getAllDescendantComponentsOf(CounterDetailViewer.class).iterator().next(); viewer.setAttribute(CounterDetailViewer.DISPLAY, CounterDetailViewer.FILTER); viewer.setAttribute(CounterDetailViewer.PROPERTY_FILTER, ADC2Utils.TYPE + " = " + PIECE); StringBuilder sb = new StringBuilder(); int mask = 0x1; for (int i = 0; i < classValues.length; ++i) { if (classValues[i] != null && !classValues[i].equals("")) { if ((classCombatSummaryValues & mask) > 0) { if (sb.length() > 0) sb.append('-'); sb.append("$sum(" + classValues[i] + ")$"); } } mask <<= 1; } mask = 0x1; for (int i = 0; i < pieceValues.length; ++i) { if (pieceValues[i] != null && !pieceValues[i].equals("")) { if ((pieceCombatSummaryValues & mask) > 0) { if (sb.length() > 0) sb.append('-'); sb.append("$sum(" + pieceValues[i] + ")$"); } } mask <<= 1; } viewer.setAttribute(CounterDetailViewer.SHOW_TEXT, Boolean.TRUE); if (sb.length() > 0) sb.append(' '); viewer.setAttribute(CounterDetailViewer.MINIMUM_DISPLAYABLE, "1"); viewer.setAttribute(CounterDetailViewer.SUMMARY_REPORT_FORMAT, sb.toString() + "($LocationName$)"); if (usePieceNames) { viewer.setAttribute(CounterDetailViewer.COUNTER_REPORT_FORMAT, PC_NAME); } viewer.setAttribute(CounterDetailViewer.UNROTATE_PIECES, Boolean.TRUE); viewer.setAttribute(CounterDetailViewer.BG_COLOR, Color.WHITE); } protected void writeClassesToArchive(GameModule gameModule) throws IOException { pieceWin.setAttribute(PieceWindow.NAME, ADD_NEW_PIECES); ListWidget list = new ListWidget(); insertComponent(list, pieceWin); for (PieceClass c : pieceClasses) { c.writeToArchive(list); } } protected void writePlayersToArchive(GameModule gameModule) { final PlayerRoster roster = gameModule.getAllDescendantComponentsOf(PlayerRoster.class).iterator().next(); final SequenceEncoder se = new SequenceEncoder(','); for (Player player : players) { if (player.allies.first() == player) // only write out if it's the first in an alliance se.append(player.getName()); } for (int i = 0; i < 2; ++i) roster.setAttribute(PlayerRoster.SIDES, se.getValue()); } // TODO make a select all cards in hand option protected void writeHandsToArchive(GameModule module) throws IOException { final int nHands = forcePools.count(HandPool.class); if (nHands == 0) return; for (Iterator<Pool> iter = forcePools.iterator(HandPool.class); iter.hasNext(); ) { HandPool pool = (HandPool) iter.next(); PlayerHand hand = new PlayerHand(); insertComponent(hand, module); if (pool.getOwner() == Player.ALL_PLAYERS) { String sides[] = new String[players.size()]; for (int i = 0; i < players.size(); ++i) { sides[i] = players.get(i).getName(); } hand.setAttribute(PrivateMap.SIDE, StringArrayConfigurer.arrayToString(sides)); } else { hand.setAttribute(PrivateMap.SIDE, pool.getOwner().getName()); } hand.setAttribute(PrivateMap.VISIBLE, Boolean.TRUE); hand.setAttribute(PrivateMap.NAME, pool.name); hand.setAttribute(PrivateMap.MARK_MOVED, GlobalOptions.NEVER); hand.setAttribute(PrivateMap.USE_LAUNCH_BUTTON, Boolean.TRUE); hand.setAttribute(PrivateMap.BUTTON_NAME, pool.getButtonName()); BoardPicker picker = hand.getBoardPicker(); final Board board = new Board(); insertComponent(board, picker); board.setConfigureName(pool.name); List<Piece> s = pool.getPieces(); if (pieces.size() > 0) { SetupStack stack = new SetupStack(); insertComponent(stack, hand); Dimension d = getMaxDeckSize(); Point p = new Point(d.width/2 + 10, d.height/2 + 10); stack.setAttribute(SetupStack.NAME, pool.name); stack.setAttribute(SetupStack.OWNING_BOARD, board.getConfigureName()); stack.setAttribute(SetupStack.X_POSITION, Integer.toString(p.x)); stack.setAttribute(SetupStack.Y_POSITION, Integer.toString(p.y)); for (Piece pc : s) { pc.writeToArchive(stack); } } } } protected void writeDecksToArchive(GameModule gameModule) throws IOException { final int nDecks = forcePools.count(DeckPool.class); if (nDecks == 0) return; final Map deckMap = new Map(); insertComponent(deckMap, gameModule); deckMap.setMapName(DECKS); deckMap.setAttribute(Map.MARK_MOVED, GlobalOptions.NEVER); deckMap.setAttribute(Map.USE_LAUNCH_BUTTON, Boolean.TRUE); deckMap.setAttribute(Map.BUTTON_NAME, DECKS); deckMap.setAttribute(Map.HOTKEY, new NamedKeyStroke(KeyStroke.getKeyStroke('D', InputEvent.CTRL_DOWN_MASK))); final BoardPicker boardPicker = deckMap.getBoardPicker(); // write force pool board final Dimension maxSize = getMaxDeckSize(); boolean vertical = maxSize.width > maxSize.height; final JPanel panel = new JPanel(); final JPanel[] deckPanels = new JPanel[nDecks]; final GridBagConstraints c = new GridBagConstraints(); c.insets = new Insets(5, 5, 5, 5); c.fill = GridBagConstraints.BOTH; c.anchor = GridBagConstraints.CENTER; panel.setLayout(new GridBagLayout()); panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); Iterator<Pool> iter = forcePools.iterator(DeckPool.class); for (int i = 0; i < nDecks; ++i) { Pool pool = iter.next(); deckPanels[i] = new JPanel(); deckPanels[i].setPreferredSize(maxSize); deckPanels[i].setMaximumSize(maxSize); deckPanels[i].setBorder(BorderFactory.createLoweredBevelBorder()); if (vertical) { c.gridy = i*2; c.gridx = 1; } else { c.gridy = 1; c.gridx = i; } c.insets.bottom = 2; c.insets.top = 5; panel.add(deckPanels[i], c); String name; if (((Cards) pool).getOwner() == Player.ALL_PLAYERS) { name = pool.name; } else if (((Cards) pool).getOwner() == Player.NO_PLAYERS) { name = pool.name; } else { name = pool.name + " (" + ((Cards) pool).getOwner().getName() + ")"; } JLabel label = new JLabel(name); label.setHorizontalAlignment(SwingConstants.CENTER); c.gridy += 1; c.insets.top = 2; c.insets.bottom = 5; panel.add(label, c); } final Dimension d = panel.getPreferredSize(); panel.setSize(d); panel.doLayout(); final BufferedImage poolImage = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB); final Graphics2D g = poolImage.createGraphics(); panel.printAll(g); // write the map image final ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(poolImage, "png", out); final byte[] imageDataArray = out.toByteArray(); final String deckImageName = "decks.png"; gameModule.getArchiveWriter().addImage(deckImageName, imageDataArray); final Board board = new Board(); insertComponent(board, boardPicker); board.setConfigureName(DECKS); board.setAttribute(Board.IMAGE, deckImageName); // create decks final Rectangle rv = new Rectangle(); iter = forcePools.iterator(DeckPool.class); for (int i = 0; i < nDecks; ++i) { Pool pool = iter.next(); DrawPile pile = new DrawPile(); insertComponent(pile, deckMap); JPanel p = deckPanels[i]; p.getBounds(rv); pile.setAttribute(DrawPile.OWNING_BOARD, DECKS); pile.setAttribute(DrawPile.X_POSITION, rv.x + rv.width/2); pile.setAttribute(DrawPile.Y_POSITION, rv.y + rv.height/2); pile.setAttribute(DrawPile.WIDTH, rv.width); pile.setAttribute(DrawPile.HEIGHT, rv.height); pile.setAttribute(DrawPile.FACE_DOWN, Deck.ALWAYS); pile.setAttribute(DrawPile.DRAW_FACE_UP, Boolean.FALSE); pile.setAttribute(DrawPile.SHUFFLE, Deck.NEVER); pile.setAttribute(DrawPile.REVERSIBLE, Boolean.FALSE); pile.setAttribute(DrawPile.ALLOW_MULTIPLE, Boolean.TRUE); pile.setAttribute(DrawPile.ALLOW_SELECT, ((Cards) pool).getOwner() == Player.ALL_PLAYERS); pile.setAttribute(DrawPile.RESHUFFLABLE, Boolean.FALSE); pile.setAttribute(DrawPile.NAME, pool.name); pile.setAttribute(DrawPile.SHUFFLE, DrawPile.USE_MENU); pile.setAttribute(DrawPile.SHUFFLE_REPORT_FORMAT, "$playerName$ reshuffles $deckName$"); pile.setAttribute(DrawPile.SHUFFLE_HOTKEY, new NamedKeyStroke(KeyStroke.getKeyStroke('S', InputEvent.CTRL_DOWN_MASK))); for (Piece pc : pool.getPieces()) { pc.writeToArchive(pile); } } } protected void writeToolbarMenuToArchive(GameModule gameModule) { final int nHands = forcePools.count(HandPool.class); if (nHands == 0) return; ToolbarMenu menu = new ToolbarMenu(); insertComponent(menu, gameModule); menu.setAttribute(ToolbarMenu.BUTTON_TEXT, "Windows"); menu.setAttribute(ToolbarMenu.TOOLTIP, "Open trays, decks, charts, and hands."); String items[] = new String[nHands+3]; items[0] = TRAY; items[1] = DECKS; int start = 2; if (infoPageName != null) { items[2] = CHARTS; start = 3; } Iterator<Pool> iter = forcePools.iterator(HandPool.class); for (int i = 0; i < nHands; ++i) { items[i+start] = iter.next().getButtonName(); } menu.setAttribute(ToolbarMenu.MENU_ITEMS, StringArrayConfigurer.arrayToString(items)); } private Dimension getMaxDeckSize() throws IOException { Dimension d = new Dimension(0,0); for (int i = 0; i < nCardSets; ++i) getCardDeck(i).getMaxSize(d); return d; } /** * Creates a board with deck stacks in which force pools are kept. * * @throws IOException */ // TODO: cards should not be accessible if they are invisible. Can still draw // invisible cards right now. protected void writeForcePoolsToArchive(GameModule gameModule) throws IOException { int nForcePools = forcePools.count(ForcePool.class); if (nForcePools == 0) return; final GameModule module = GameModule.getGameModule(); final Map forcePoolMap = new Map(); insertComponent(forcePoolMap, module); forcePoolMap.setMapName(TRAY); forcePoolMap.setAttribute(Map.MARK_MOVED, GlobalOptions.NEVER); forcePoolMap.setAttribute(Map.USE_LAUNCH_BUTTON, Boolean.TRUE); forcePoolMap.setAttribute(Map.BUTTON_NAME, TRAY); forcePoolMap.setAttribute(Map.HOTKEY, new NamedKeyStroke(KeyStroke.getKeyStroke('T', InputEvent.CTRL_DOWN_MASK))); final BoardPicker boardPicker = forcePoolMap.getBoardPicker(); // write force pool board final Dimension modalSize = getSet().getModalSize(); modalSize.height = modalSize.height * 3 / 2; modalSize.width = modalSize.width * 3 / 2; final JPanel panel = new JPanel(); final JPanel[] deckPanels = new JPanel[nForcePools]; final GridBagConstraints c = new GridBagConstraints(); c.insets = new Insets(5, 5, 5, 5); c.fill = GridBagConstraints.BOTH; c.anchor = GridBagConstraints.CENTER; panel.setLayout(new GridBagLayout()); panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); final int nColumns = (int) Math.sqrt((double) nForcePools); Iterator<Pool> iter = forcePools.iterator(ForcePool.class); for (int i = 0; i < nForcePools; ++i) { ForcePool fp = (ForcePool) iter.next(); deckPanels[i] = new JPanel(); deckPanels[i].setBorder(BorderFactory.createLoweredBevelBorder()); c.gridy = (i/nColumns)*2; c.gridx = i%nColumns; c.insets.bottom = 2; c.insets.top = 5; panel.add(deckPanels[i], c); JLabel label = new JLabel(fp.name); label.setHorizontalAlignment(SwingConstants.CENTER); c.gridy += 1; c.insets.top = 2; c.insets.bottom = 5; panel.add(label, c); Dimension d = label.getPreferredSize(); if (d.width > modalSize.width) modalSize.width = d.width; } for (JPanel p : deckPanels) p.setPreferredSize(modalSize); final Dimension d = panel.getPreferredSize(); panel.setSize(d); panel.doLayout(); final BufferedImage forcePool = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB); final Graphics2D g = forcePool.createGraphics(); panel.printAll(g); // write the map image final ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(forcePool, "png", out); final byte[] imageDataArray = out.toByteArray(); module.getArchiveWriter().addImage(FORCE_POOL_PNG, imageDataArray); final Board board = new Board(); insertComponent(board, boardPicker); board.setConfigureName(TRAY); board.setAttribute(Board.IMAGE, FORCE_POOL_PNG); // create decks final Rectangle rv = new Rectangle(); iter = forcePools.iterator(ForcePool.class); for (int i = 0; i < nForcePools; ++i) { ForcePool fp = (ForcePool) iter.next(); DrawPile pile = new DrawPile(); insertComponent(pile, forcePoolMap); JPanel p = deckPanels[i]; p.getBounds(rv); pile.setAttribute(DrawPile.OWNING_BOARD, TRAY); pile.setAttribute(DrawPile.X_POSITION, rv.x + rv.width/2); pile.setAttribute(DrawPile.Y_POSITION, rv.y + rv.height/2); pile.setAttribute(DrawPile.WIDTH, rv.width); pile.setAttribute(DrawPile.HEIGHT, rv.height); pile.setAttribute(DrawPile.FACE_DOWN, Deck.NEVER); pile.setAttribute(DrawPile.DRAW_FACE_UP, Boolean.TRUE); pile.setAttribute(DrawPile.SHUFFLE, Deck.NEVER); pile.setAttribute(DrawPile.REVERSIBLE, Boolean.FALSE); pile.setAttribute(DrawPile.ALLOW_MULTIPLE, Boolean.FALSE); pile.setAttribute(DrawPile.RESHUFFLABLE, Boolean.FALSE); pile.setAttribute(DrawPile.NAME, fp.name); for (Piece pc : fp.getPieces()) { pc.writeToArchive(pile); } } } // add option to show only top piece just like decks. protected void writeSetupStacksToArchive(GameModule gameModule) throws IOException { final Map mainMap = getMainMap(); final Point offset = getMap().getCenterOffset(); for (java.util.Map.Entry<Integer,ArrayList<Piece>> en : stacks.entrySet()) { final int hex = en.getKey(); Point p = getMap().indexToPosition(hex); if (p == null) continue; final ArrayList<Piece> s = en.getValue(); SetupStack stack = new SetupStack(); insertComponent(stack, mainMap); p.translate(offset.x, offset.y); String location = mainMap.locationName(p); stack.setAttribute(SetupStack.NAME, location); Board board = getMap().getBoard(); stack.setAttribute(SetupStack.OWNING_BOARD, board.getConfigureName()); MapGrid mg = board.getGrid(); Zone z = null; if (mg instanceof ZonedGrid) z = ((ZonedGrid) mg).findZone(p); stack.setAttribute(SetupStack.X_POSITION, Integer.toString(p.x)); stack.setAttribute(SetupStack.Y_POSITION, Integer.toString(p.y)); if (z != null) { try { if (mg.getLocation(location) != null) { assert(mg.locationName(mg.getLocation(location)).equals(location)); stack.setAttribute(SetupStack.USE_GRID_LOCATION, true); stack.setAttribute(SetupStack.LOCATION, location); } } catch(BadCoords e) {} } for (Piece pc : s) { pc.writeToArchive(stack); } } } protected MapBoard getMap() { return map; } protected SymbolSet getSet() { return getMap().getSet(); } @Override public boolean isValidImportFile(File f) throws IOException { DataInputStream in = null; try { in = new DataInputStream(new FileInputStream(f)); int header = in.readByte(); boolean valid = header == -3 || header == -2; in.close(); return valid; } finally { IOUtils.closeQuietly(in); } } }