package me.desht.chesscraft.chess;
import me.desht.chesscraft.ChessCraft;
import me.desht.chesscraft.ChessPersistence;
import me.desht.chesscraft.Messages;
import me.desht.chesscraft.enums.BoardRotation;
import me.desht.chesscraft.event.ChessBoardCreatedEvent;
import me.desht.chesscraft.event.ChessBoardDeletedEvent;
import me.desht.chesscraft.exceptions.ChessException;
import me.desht.chesscraft.util.ChessUtils;
import me.desht.chesscraft.util.TerrainBackup;
import me.desht.dhutils.LogUtils;
import me.desht.dhutils.MiscUtil;
import me.desht.dhutils.PermissionUtils;
import me.desht.dhutils.PersistableLocation;
import me.desht.dhutils.block.BlockType;
import me.desht.dhutils.cuboid.Cuboid;
import me.desht.dhutils.cuboid.Cuboid.CuboidDirection;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.material.MaterialData;
import java.io.File;
import java.util.*;
public class BoardViewManager {
private static BoardViewManager instance = null;
private final Map<String, BoardView> chessBoards = new HashMap<String, BoardView>();
private final Map<String, Set<File>> deferred = new HashMap<String, Set<File>>();
private PersistableLocation globalTeleportOutDest = null;
private final List<Cuboid> flightRegions = new ArrayList<Cuboid>();
private BoardViewManager() {
}
public static synchronized BoardViewManager getManager() {
if (instance == null) {
instance = new BoardViewManager();
}
return instance;
}
@SuppressWarnings("CloneDoesntCallSuperClone")
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* @return the globalTeleportOutDest
*/
public Location getGlobalTeleportOutDest() {
return globalTeleportOutDest == null ? null : globalTeleportOutDest.getLocation();
}
/**
* @param globalTeleportOutDest the globalTeleportOutDest to set
*/
public void setGlobalTeleportOutDest(Location globalTeleportOutDest) {
this.globalTeleportOutDest = globalTeleportOutDest == null ? null : new PersistableLocation(globalTeleportOutDest);
}
public void registerView(BoardView view) {
chessBoards.put(view.getName(), view);
Bukkit.getPluginManager().callEvent(new ChessBoardCreatedEvent(view));
}
private void unregisterBoardView(String name) {
BoardView bv;
try {
bv = getBoardView(name);
chessBoards.remove(name);
Bukkit.getPluginManager().callEvent(new ChessBoardDeletedEvent(bv));
} catch (ChessException e) {
LogUtils.warning("removeBoardView: unknown board name " + name);
}
}
public void deleteBoardView(String name, boolean permanent) {
BoardView bv = getBoardView(name);
if (permanent) {
if (bv.getGame() != null) {
throw new ChessException(Messages.getString("ChessCommandExecutor.boardCantBeDeleted", name, bv.getGame().getName()));
}
bv.restoreTerrain();
ChessCraft.getInstance().getPersistenceHandler().unpersist(bv);
} else {
if (bv.getGame() != null) {
ChessGameManager.getManager().deleteGame(bv.getGame().getName(), false);
}
bv.getChessBoard().getChessSet().syncToPosition(null, bv.getChessBoard());
}
unregisterBoardView(name);
}
public boolean boardViewExists(String name) {
return chessBoards.containsKey(name);
}
public BoardView getBoardView(String name) throws ChessException {
if (!chessBoards.containsKey(name)) {
if (chessBoards.size() > 0) {
// try "fuzzy" search
Set<String> strings = chessBoards.keySet();
String keys[] = strings.toArray(new String[strings.size()]);
String matches[] = ChessUtils.fuzzyMatch(name, keys, 3);
if (matches.length == 1) {
return chessBoards.get(matches[0]);
} else {
// partial-name search
int k = -1, c = 0;
name = name.toLowerCase();
for (int i = 0; i < keys.length; ++i) {
if (keys[i].toLowerCase().startsWith(name)) {
k = i;
++c;
}
}
if (k >= 0 && c == 1) {
return chessBoards.get(keys[k]);
}
}
}
throw new ChessException(Messages.getString("BoardView.noSuchBoard", name)); //$NON-NLS-1$ //$NON-NLS-2$
}
return chessBoards.get(name);
}
public Collection<BoardView> listBoardViews() {
return chessBoards.values();
}
public Collection<BoardView> listBoardViewsSorted() {
SortedSet<String> sorted = new TreeSet<String>(chessBoards.keySet());
List<BoardView> res = new ArrayList<BoardView>();
for (String name : sorted) {
res.add(chessBoards.get(name));
}
return res;
}
/**
* Get a board that does not have a game running.
*
* @return the first free board found
* @throws ChessException if no free board was found
*/
public BoardView getFreeBoard() throws ChessException {
for (BoardView bv : listBoardViews()) {
if (bv.getGame() == null && !bv.isDesigning()) {
return bv;
}
}
throw new ChessException(Messages.getString("BoardView.noFreeBoards")); //$NON-NLS-1$
}
/**
* Check if a location is any part of any board including the frame & enclosure.
*
* @param loc location to check
* @return the boardview that matches, or null if none
*/
public BoardView partOfChessBoard(Location loc) {
return partOfChessBoard(loc, 0);
}
/**
* Check if a location is any part of any board including the frame & enclosure.
*
* @param loc location to check
* @param fudge fudge factor - check a larger area around the board
* @return the boardview that matches, or null if none
*/
public BoardView partOfChessBoard(Location loc, int fudge) {
for (BoardView bv : listBoardViews()) {
if (bv.isPartOfBoard(loc, fudge)) {
return bv;
}
}
return null;
}
/**
* Get the flight region for the given location, if any.
*
* @param loc the location to check
* @return the flight region for the location, or null if not in a flight region
*/
public Cuboid getFlightRegion(Location loc) {
for (Cuboid c : flightRegions) {
if (c.contains(loc)) {
return c;
}
}
return null;
}
/**
* Check if location is above a board square but below the roof
*
* @param loc location to check
* @return the boardview that matches, or null if none
*/
public BoardView aboveChessBoard(Location loc) {
for (BoardView bv : listBoardViews()) {
if (bv.isAboveBoard(loc)) {
return bv;
}
}
return null;
}
/**
* Check if location is part of a board square
*
* @param loc location to check
* @return the boardview that matches, or null if none
*/
public BoardView onChessBoard(Location loc) {
for (BoardView bv : listBoardViews()) {
if (bv.isOnBoard(loc)) {
return bv;
}
}
return null;
}
/**
* Teleport the player in a sensible manner, depending on where they are now.
*
* @param player the player to check
* @throws ChessException
*/
public void teleportOut(Player player) throws ChessException {
PermissionUtils.requirePerms(player, "chesscraft.commands.teleport");
BoardView bv = partOfChessBoard(player.getLocation(), 0);
Location prev = ChessCraft.getInstance().getPlayerTracker().getLastPos(player);
if (bv != null && bv.hasTeleportDestination()) {
// board has a specific location defined
Location loc = bv.getTeleportDestination();
ChessCraft.getInstance().getPlayerTracker().teleportPlayer(player, loc);
} else if (bv != null && globalTeleportOutDest != null) {
ChessCraft.getInstance().getPlayerTracker().teleportPlayer(player, getGlobalTeleportOutDest());
} else if (bv != null && (prev == null || partOfChessBoard(prev, 0) == bv)) {
// try to get the player out of this board safely
Location loc = bv.findSafeLocationOutside();
if (loc != null) {
ChessCraft.getInstance().getPlayerTracker().teleportPlayer(player, loc);
} else {
ChessCraft.getInstance().getPlayerTracker().teleportPlayer(player, player.getWorld().getSpawnLocation());
MiscUtil.errorMessage(player, Messages.getString("ChessCommandExecutor.goingToSpawn")); //$NON-NLS-1$
}
} else if (prev != null) {
// go back to previous location
ChessCraft.getInstance().getPlayerTracker().teleportPlayer(player, prev);
} else {
throw new ChessException(Messages.getString("ChessCommandExecutor.notOnChessboard")); //$NON-NLS-1$
}
}
/**
* Convenience method to create a new board and do all the associated setup tasks.
*
* @param boardName name of the new board
* @param loc location of the A1 centre
* @param style the board style name
* @param pieceStyle the piece style name
* @return a fully initialised and painted board
*/
public BoardView createBoard(String boardName, Location loc, BoardRotation rotation, String style, String pieceStyle) {
BoardView view = new BoardView(boardName, loc, rotation, style, pieceStyle);
registerView(view);
if (ChessCraft.getInstance().getWorldEdit() != null) {
TerrainBackup.save(view);
}
view.save();
view.paintAll();
return view;
}
/**
* Mark a board as deferred loading - its world wasn't available so we'll record the board
* file name for later.
*
* @param worldName name if the world
* @param f file the board is being loaded from
*/
public void deferLoading(String worldName, File f) {
if (!deferred.containsKey(worldName)) {
deferred.put(worldName, new HashSet<File>());
}
deferred.get(worldName).add(f);
}
/**
* Load any deferred boards for the given world.
*
* @param worldName name of the world
*/
public void loadDeferred(String worldName) {
if (!deferred.containsKey(worldName)) {
return;
}
LogUtils.info("loading deferred boards for " + worldName);
for (File f : deferred.get(worldName)) {
ChessCraft.getInstance().getPersistenceHandler().loadBoard(f);
}
deferred.get(worldName).clear();
}
/**
* Called when a world is unloaded. Put any boards in that world back on the deferred list.
*
* @param worldName name of the world
*/
public void unloadBoardsForWorld(String worldName) {
for (BoardView bv : new ArrayList<BoardView>(listBoardViews())) {
if (bv.getWorldName().equals(worldName)) {
BoardViewManager.getManager().deleteBoardView(bv.getName(), false);
File f = new File(bv.getSaveDirectory(), ChessPersistence.makeSafeFileName(bv.getName()) + ".yml");
deferLoading(bv.getWorldName(), f);
LogUtils.info("unloaded board '" + bv.getName() + "' (world has been unloaded)");
}
}
}
/**
* Get the boardview that the given chunk is in, if any.
*
* @param chunk the chunk to check
* @return the boardview containing the chunk, or null
*/
public BoardView getBoardViewForChunk(Chunk chunk) {
for (BoardView bv : new ArrayList<BoardView>(listBoardViews())) {
Cuboid c = bv.getOuterBounds();
if (!c.getWorld().equals(chunk.getWorld())) {
continue;
}
for (Chunk boardChunk : c.getChunks()) {
if (boardChunk.getX() == chunk.getX() && boardChunk.getZ() == chunk.getZ()) {
return bv;
}
}
}
return null;
}
/**
* Cache the regions in which flight is allowed. We do this to avoid calculation in the
* code which is (frequently) called from the PlayerMoveEvent handler in the flight listener.
*/
public void recalculateFlightRegions() {
int above = ChessCraft.getInstance().getConfig().getInt("flying.upper_limit");
int outside = ChessCraft.getInstance().getConfig().getInt("flying.outer_limit");
flightRegions.clear();
for (BoardView bv : listBoardViews()) {
Cuboid c = bv.getOuterBounds();
MaterialData mat = bv.getChessBoard().getBoardStyle().getEnclosureMaterial();
if (BlockType.canPassThrough(mat.getItemTypeId())) {
c = c.expand(CuboidDirection.Up, Math.max(5, (c.getSizeY() * above) / 100));
c = c.outset(CuboidDirection.Horizontal, Math.max(5, (c.getSizeX() * outside) / 100));
}
flightRegions.add(c);
}
}
public BoardView findBoardForGame(ChessGame game) {
for (BoardView bv : listBoardViews()) {
if (bv.getGame() != null && bv.getGame().getName().equals(game.getName())) {
return bv;
}
}
return null;
}
}