/* * Copyright (c) 2011-2013 by Brent Easton * * 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.build; import java.util.ArrayList; import java.util.HashMap; import VASSAL.build.widget.PieceSlot; import VASSAL.counters.BasicPiece; import VASSAL.counters.Decorator; import VASSAL.counters.GamePiece; import VASSAL.counters.PieceCloner; import VASSAL.counters.PlaceMarker; import VASSAL.counters.Properties; /** * Build a cross-reference of all GpId-able elements in a module or ModuleExtension, * Check for missing, duplicate or illegal GamePieceId's * Update if necessary * */ public class GpIdChecker { protected GpIdSupport gpIdSupport; protected int maxId; protected boolean useName = false; final HashMap<String, SlotElement> goodSlots = new HashMap<String, SlotElement>(); final ArrayList<SlotElement> errorSlots = new ArrayList<SlotElement>(); public GpIdChecker() { this(null); } public GpIdChecker(GpIdSupport gpIdSupport) { this.gpIdSupport = gpIdSupport; maxId = -1; } public GpIdChecker(boolean useName) { this(); this.useName = useName; } /** * Add a PieceSlot to our cross-reference and any PlaceMarker * traits it contains. * * @param pieceSlot */ public void add(PieceSlot pieceSlot) { testGpId(pieceSlot.getGpId(), new SlotElement(pieceSlot)); // PlaceMarker traits within the PieceSlot definition also contain GpId's. GamePiece gp = pieceSlot.getPiece(); checkTrait(gp, pieceSlot); } /** * Check for PlaceMarker traits in a GamePiece and add them to * the cross-reference * * @param gp * @param slot */ protected void checkTrait(GamePiece gp, PieceSlot slot) { if (gp == null || gp instanceof BasicPiece) { return; } if (gp instanceof PlaceMarker) { final PlaceMarker pm = (PlaceMarker) gp; testGpId (pm.getGpId(), new SlotElement(pm, slot)); } checkTrait(((Decorator) gp).getInner(), slot); } /** * Validate a GamePieceId. * - non-null * - Integer * - Not a duplicate of any other GpId * Keep a list of the good Slots and the slots with errors. * Also track the maximum GpId * * @param id * @param element */ protected void testGpId(String id, SlotElement element) { /* * If this has been called from a ModuleExtension, the GpId is prefixed with * the Extension Id. Remove the Extension Id and just process the numerid part. */ if (id.contains(":")) { id = id.split(":")[1]; } if (id == null || id.length() == 0) { // gpid not generated yet? errorSlots.add(element); } else { if (goodSlots.get(id) != null) { // duplicate gpid? errorSlots.add(element); } try { final int iid = Integer.parseInt(id); goodSlots.put(id, element); // gpid is good. if (iid >= maxId) { maxId = iid+1; } } catch (Exception e) { errorSlots.add(element); // non-numeric gpid? } } } /** * Where any errors found? * @return */ public boolean hasErrors() { return errorSlots.size() > 0; } /** * Repair any errors * - Update the next GpId in the module if necessary * - Generate new GpId's for slots with errors. */ public void fixErrors() { if (maxId >= gpIdSupport.getNextGpId()) { gpIdSupport.setNextGpId(maxId+1); } for (SlotElement slotElement : errorSlots) { slotElement.updateGpId(); } } /** * Locate the SlotElement that matches oldPiece and return a new GamePiece * created from that Slot. * * * * @param oldPiece * @return */ public GamePiece createUpdatedPiece (GamePiece oldPiece) { // Find a slot with a matching gpid final String gpid = (String) oldPiece.getProperty(Properties.PIECE_ID); if (gpid != null && gpid.length() > 0) { final SlotElement element = goodSlots.get(gpid); if (element != null) { return element.createPiece(oldPiece); } } // Failed to find a slot by gpid, try by matching piece name if option selected if (useName) { final String oldPieceName = Decorator.getInnermost(oldPiece).getName(); for (SlotElement el : goodSlots.values()) { final GamePiece newPiece = el.getPiece(); final String newPieceName = Decorator.getInnermost(newPiece).getName(); if (oldPieceName.equals(newPieceName)) { return el.createPiece(oldPiece); } } } return oldPiece; } public boolean findUpdatedPiece (GamePiece oldPiece) { // Find a slot with a matching gpid final String gpid = (String) oldPiece.getProperty(Properties.PIECE_ID); if (gpid != null && gpid.length() > 0) { final SlotElement element = goodSlots.get(gpid); if (element != null) { return true; } } // Failed to find a slot by gpid, try by matching piece name if option selected if (useName) { final String oldPieceName = Decorator.getInnermost(oldPiece).getName(); for (SlotElement el : goodSlots.values()) { final GamePiece newPiece = el.getPiece(); final String newPieceName = Decorator.getInnermost(newPiece).getName(); if (oldPieceName.equals(newPieceName)) { return true; } } } return false; } /** * Wrapper class for components that contain a GpId - They will all be either * PieceSlot components or PlaceMarker Decorator's. * Ideally we would add an interface to these components, but this * will break any custom code based on PlaceMarker * */ class SlotElement { private PieceSlot slot; private PlaceMarker marker; private String id; public SlotElement() { slot = null; marker = null; } public SlotElement(PieceSlot ps) { this(); slot = ps; id = ps.getGpId(); } public SlotElement(PlaceMarker pm, PieceSlot ps) { this(); marker = pm; slot = ps; id = pm.getGpId(); } public String getGpId() { return id; } public void updateGpId() { if (marker == null) { slot.updateGpId(); } else { marker.updateGpId(); } } public GamePiece getPiece() { if (slot == null) { return marker; } else { return slot.getPiece(); } } /** * Create a new GamePiece based on this Slot Element. Use oldPiece * to copy state information over to the new piece. * * @param oldPiece Old Piece for state information * @return New Piece */ public GamePiece createPiece(GamePiece oldPiece) { GamePiece newPiece = (slot != null) ? slot.getPiece() : marker.createMarker(); // The following two steps create a complete new GamePiece with all // prototypes expanded newPiece = PieceCloner.getInstance().clonePiece(newPiece); copyState(oldPiece, newPiece); newPiece.setProperty(Properties.PIECE_ID, getGpId()); return newPiece; } /** * Copy as much state information as possible from the old * piece to the new piece * * @param oldPiece Piece to copy state from * @param newPiece Piece to copy state to */ protected void copyState(GamePiece oldPiece, GamePiece newPiece) { GamePiece p = newPiece; while (p != null) { if (p instanceof BasicPiece) { ((BasicPiece) p).setState(((BasicPiece) Decorator.getInnermost(oldPiece)).getState()); p = null; } else { final Decorator decorator = (Decorator) p; final String type = decorator.myGetType(); final String newState = findStateFromType(oldPiece, type, p.getClass()); if (newState != null && newState.length() > 0) { decorator.mySetState(newState); } p = decorator.getInner(); } } } /** * Locate a Decorator in the old piece that has the exact same * type as the new Decorator and return it's state * * @param oldPiece Old piece to search * @param typeToFind Type to match * @param classToFind Class to match * @return */ protected String findStateFromType(GamePiece oldPiece, String typeToFind, Class<? extends GamePiece> classToFind) { GamePiece p = oldPiece; while (p != null && !(p instanceof BasicPiece)) { final Decorator d = (Decorator) Decorator.getDecorator(p, classToFind); if (d != null) { if (d.getClass().equals(classToFind)) { if (d.myGetType().equals(typeToFind)) { return d.myGetState(); } } p = d.getInner(); } else p = null; } return null; } } }