/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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 General Public License for more details.
*/
package illarion.mapedit.data;
import illarion.common.util.CopyrightHeader;
import illarion.mapedit.Lang;
import illarion.mapedit.crash.exceptions.FormatCorruptedException;
import illarion.mapedit.data.formats.DataType;
import illarion.mapedit.data.formats.Decoder;
import illarion.mapedit.data.formats.DecoderFactory;
import illarion.mapedit.events.menu.MapLoadErrorEvent;
import illarion.mapedit.events.menu.MapLoadedEvent;
import org.bushe.swing.event.EventBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class takes care of loading and saving maps
*
* @author Tim
*/
public final class MapIO {
@Nonnull
private static final Logger LOGGER = LoggerFactory.getLogger(MapIO.class);
@Nonnull
private static final String HEADER_V = "V:";
@Nonnull
private static final String HEADER_L = "L:";
@Nonnull
private static final String HEADER_X = "X:";
@Nonnull
private static final String HEADER_Y = "Y:";
@Nonnull
private static final String HEADER_W = "W:";
@Nonnull
private static final String HEADER_H = "H:";
@Nonnull
public static final String EXT_WARP = ".warps.txt";
@Nonnull
public static final String EXT_ITEM = ".items.txt";
@Nonnull
public static final String EXT_TILE = ".tiles.txt";
@Nonnull
public static final String EXT_ANNO = ".annot.txt";
private static final char NEWLINE = '\n';
@Nonnull
private static final Pattern VERSION_PATTERN = Pattern.compile("V: (\\d+)");
@Nonnull
private static final CopyrightHeader COPYRIGHT_HEADER = new CopyrightHeader(80, null, null, "# ", null);
@Nonnull
private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
@Nonnull
private static final Charset CHARSET = Charset.forName("ISO-8859-1");
@Nonnull
private static final DecoderFactory DECODER_FACTORY = new DecoderFactory();
private MapIO() {
}
/**
* Loads a map from a specified file
*
* @param path the path
* @param name the map name
*/
public static void loadMap(Path path, String name) {
new Thread(() -> {
try {
EventBus.publish(new MapLoadedEvent(loadMapThread(path, name)));
} catch (FormatCorruptedException ex) {
LOGGER.warn("Format wrong.", ex);
EventBus.publish(new MapLoadErrorEvent(ex.getMessage()));
} catch (IOException ex) {
LOGGER.warn("Can't load map", ex);
EventBus.publish(new MapLoadErrorEvent(Lang.getMsg("gui.error.LoadMap")));
}
}).start();
}
private static final class LoadFileCallable implements Callable<List<String>> {
@Nonnull
private final Path file;
private LoadFileCallable(@Nonnull Path file) {
this.file = file;
}
@Override
public List<String> call() throws Exception {
try {
return Files.readAllLines(file, CHARSET);
} catch (IOException e) {
return Collections.emptyList();
}
}
}
private static final class LoadMapDataCallable implements Callable<Void> {
@Nonnull
private final DataType type;
@Nonnull
private final Decoder decoder;
@Nonnull
private final List<String> lines;
private LoadMapDataCallable(
@Nonnull DataType type, @Nonnull Decoder decoder, @Nonnull List<String> lines) {
this.type = type;
this.decoder = decoder;
this.lines = lines;
}
@Nullable
@Override
public Void call() throws Exception {
int size = lines.size();
for (int i = 0; i < size; i++) {
decoder.decodeLine(type, lines.get(i), i);
}
return null;
}
}
@Nullable
public static Map loadMapThread(@Nonnull Path path, @Nonnull String name) throws IOException {
LOGGER.debug("Load map {} at {}", name, path);
// Open the streams for all 3 files, containing the map data
Path tileFile = path.resolve(name + EXT_TILE);
Path itemFile = path.resolve(name + EXT_ITEM);
Path warpFile = path.resolve(name + EXT_WARP);
Path annoFile = path.resolve(name + EXT_ANNO);
Future<List<String>> tileLoadFuture = EXECUTOR_SERVICE.submit(new LoadFileCallable(tileFile));
Future<List<String>> itemLoadFuture = EXECUTOR_SERVICE.submit(new LoadFileCallable(itemFile));
Future<List<String>> warpLoadFuture = EXECUTOR_SERVICE.submit(new LoadFileCallable(warpFile));
Future<List<String>> annoLoadFuture = EXECUTOR_SERVICE.submit(new LoadFileCallable(annoFile));
try {
List<String> tileLines = tileLoadFuture.get();
Iterator<String> tileLinesItr = tileLines.iterator();
Decoder decoder = null;
int i = 0;
while (tileLinesItr.hasNext()) {
i++;
String line = tileLinesItr.next();
if ((line == null) || line.startsWith("#")) {
continue;
}
Matcher versionLineMatcher = VERSION_PATTERN.matcher(line);
if (versionLineMatcher.find()) {
String version = versionLineMatcher.group(1);
decoder = DECODER_FACTORY.getDecoder(Integer.parseInt(version), name, path);
break;
}
}
if (decoder == null) {
throw new IOException("Failed to find required version number line.");
}
while (tileLinesItr.hasNext()) {
i++;
String line = tileLinesItr.next();
decoder.decodeLine(DataType.Tiles, line, i);
}
List<Callable<Void>> loadingTasks = new ArrayList<>();
loadingTasks.add(new LoadMapDataCallable(DataType.Items, decoder, itemLoadFuture.get()));
loadingTasks.add(new LoadMapDataCallable(DataType.WarpPoints, decoder, warpLoadFuture.get()));
loadingTasks.add(new LoadMapDataCallable(DataType.Annotations, decoder, annoLoadFuture.get()));
EXECUTOR_SERVICE.invokeAll(loadingTasks);
Map m = decoder.getDecodedMap();
if (m == null) {
throw new IOException("No map was created by the decoder.");
}
LOGGER.debug("W={}; H={}; X={}; Y={}; L={};", m.getWidth(), m.getHeight(), m.getX(), m.getY(), m.getZ());
return m;
} catch (@Nonnull InterruptedException | ExecutionException e) {
throw new IOException("Error while loading map.", e);
}
}
/**
* Loads the map, with the map name and path, stored in the map object
*
* @param map the map to save
* @throws IOException
*/
public static void saveMap(@Nonnull Map map) throws IOException {
saveMap(map, map.getName(), map.getPath());
}
/**
* Loads the map, with specified the map name and path.
*
* @param map the Map
* @param name map name
* @param path path to map files
* @throws IOException
*/
public static void saveMap(
@Nonnull Map map, @Nonnull String name, @Nonnull Path path) throws IOException {
Path tileFile = path.resolve(name + EXT_TILE);
Path itemFile = path.resolve(name + EXT_ITEM);
Path warpFile = path.resolve(name + EXT_WARP);
Path annoFile = path.resolve(name + EXT_ANNO);
try (BufferedWriter tileOutput = Files.newBufferedWriter(tileFile, CHARSET)) {
try (BufferedWriter itemOutput = Files.newBufferedWriter(itemFile, CHARSET)) {
try (BufferedWriter warpOutput = Files.newBufferedWriter(warpFile, CHARSET)) {
try (BufferedWriter annoOutput = Files.newBufferedWriter(annoFile, CHARSET)) {
COPYRIGHT_HEADER.writeTo(tileOutput);
COPYRIGHT_HEADER.writeTo(itemOutput);
COPYRIGHT_HEADER.writeTo(warpOutput);
COPYRIGHT_HEADER.writeTo(annoOutput);
writeHeader(tileOutput, HEADER_V, 2);
writeHeader(tileOutput, HEADER_L, map.getZ());
writeHeader(tileOutput, HEADER_X, map.getX());
writeHeader(tileOutput, HEADER_Y, map.getY());
writeHeader(tileOutput, HEADER_W, map.getWidth());
writeHeader(tileOutput, HEADER_H, map.getHeight());
MapIterator itr = map.iterator();
while (itr.hasNext()) {
MapTile tile = itr.next();
int x = itr.getCurrentX();
int y = itr.getCurrentY();
// <dx>;<dy>;<tileID>;<musicID>
writeLine(tileOutput, String.format("%d;%d;%s", x, y, tile));
if (tile.hasAnnotation()) {
writeLine(annoOutput, String.format("%d;%d;0;%s", x, y, tile.getAnnotation()));
}
List<MapItem> items = tile.getMapItems();
if (items != null) {
for (int i = 0; i < items.size(); i++) {
// <dx>;<dy>;<item ID>;<quality>[;<data value>[;...]]
writeLine(itemOutput, String.format("%d;%d;%s", x, y, items.get(i)));
if (items.get(i).hasAnnotation()) {
writeLine(annoOutput, String.format("%d;%d;%d;%s", x, y, i + 1,
items.get(i).getAnnotation()));
}
}
}
MapWarpPoint warp = tile.getMapWarpPoint();
if (warp != null) {
writeLine(warpOutput, String.format("%d;%d;%s", x, y, warp));
}
}
}
}
}
}
}
private static void writeHeader(@Nonnull Writer writer, @Nonnull String header, int value) throws IOException {
writer.write(header);
writer.write(' ');
writer.write(Integer.toString(value));
writer.write(NEWLINE);
}
private static void writeLine(@Nonnull BufferedWriter writer, @Nonnull Object... args) throws IOException {
for (int i = 0; i < args.length; ++i) {
writer.write(args[i].toString());
if (i < (args.length - 1)) {
writer.write(';');
} else {
writer.write(NEWLINE);
}
}
}
private static void writeLine(@Nonnull BufferedWriter writer, @Nonnull String str) throws IOException {
writer.write(str);
writer.write(NEWLINE);
}
}