package com.faforever.client.map;
import com.google.common.io.LittleEndianDataInputStream;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.WritableImage;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import javax.imageio.ImageIO;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static com.github.nocatch.NoCatch.noCatch;
import static java.awt.Image.SCALE_SMOOTH;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
import static java.nio.file.Files.isRegularFile;
import static java.nio.file.Files.list;
import static org.luaj.vm2.lib.jse.JsePlatform.standardGlobals;
public final class PreviewGenerator {
private static final double RESOURCE_ICON_RATIO = 20 / 1024d;
private static final String MASS_IMAGE = "/images/map_markers/mass.png";
private static final String HYDRO_IMAGE = "/images/map_markers/hydro.png";
private static final String ARMY_IMAGE = "/images/map_markers/army.png";
private static final String ARMY_PREFIX = "ARMY_";
private PreviewGenerator() {
throw new AssertionError("Not instantiatable");
}
public static javafx.scene.image.Image generatePreview(Path mapFolder, int width, int height) {
Path mapPath = noCatch(() -> list(mapFolder))
.filter(file -> file.getFileName().toString().endsWith(".scmap"))
.findFirst()
.orElseThrow(() -> new RuntimeException("No map file was found in: " + mapFolder.toAbsolutePath()));
return noCatch(() -> {
MapData mapData = parseMap(mapPath);
BufferedImage previewImage = getDdsImage(mapData);
previewImage = scale(previewImage, width, height);
addMarkers(previewImage, mapData);
return SwingFXUtils.toFXImage(previewImage, new WritableImage(width, height));
});
}
private static MapData parseMap(Path mapPath) throws IOException {
MapData mapData = new MapData();
try (LittleEndianDataInputStream mapInput = new LittleEndianDataInputStream(Files.newInputStream(mapPath))) {
mapInput.skip(16);
mapData.setWidth((int) mapInput.readFloat());
mapData.setHeight((int) mapInput.readFloat());
mapInput.skip(6);
int ddsSize = mapInput.readInt();
// Skip DDS header
mapInput.skipBytes(128);
byte[] buffer = new byte[ddsSize - 128];
mapInput.readFully(buffer);
mapData.setDdsData(buffer);
Path lua = Paths.get(mapPath.toAbsolutePath().toString().replace(".scmap", "_save.lua"));
if (isRegularFile(lua)) {
Globals globals = standardGlobals();
globals.baselib.load(globals.load(new InputStreamReader(PreviewGenerator.class.getResourceAsStream("/lua/faf.lua")), "irrelevant"));
globals.loadfile(lua.toAbsolutePath().toString()).invoke();
LuaTable markers = globals.get("Scenario").get("MasterChain").get("_MASTERCHAIN_").get("Markers").checktable();
mapData.setMarkers(markers);
}
}
return mapData;
}
private static BufferedImage getDdsImage(MapData mapData) throws IOException {
byte[] ddsData = mapData.getDdsData();
int ddsDimension = (int) (Math.sqrt(ddsData.length) / 2);
bgraToAbgr(ddsData);
BufferedImage previewImage = new BufferedImage(ddsDimension, ddsDimension, BufferedImage.TYPE_4BYTE_ABGR);
previewImage.setData(Raster.createRaster(previewImage.getSampleModel(), new DataBufferByte(ddsData, ddsData.length), new Point()));
return previewImage;
}
private static BufferedImage scale(BufferedImage previewImage, double width, double height) {
int targetWidth = width < 1 ? 1 : (int) width;
int targetHeight = height < 1 ? 1 : (int) height;
Image image = previewImage.getScaledInstance(targetWidth, targetHeight, SCALE_SMOOTH);
BufferedImage scaledImage = new BufferedImage(targetWidth, targetHeight, TYPE_INT_ARGB);
Graphics graphics = scaledImage.createGraphics();
graphics.drawImage(image, 0, 0, null);
graphics.dispose();
return scaledImage;
}
private static void addMarkers(BufferedImage previewImage, MapData mapData) throws IOException {
float width = previewImage.getWidth();
float height = previewImage.getHeight();
Image massImage = scale(readImage(MASS_IMAGE), RESOURCE_ICON_RATIO * width, RESOURCE_ICON_RATIO * height);
Image hydroImage = scale(readImage(HYDRO_IMAGE), RESOURCE_ICON_RATIO * width, RESOURCE_ICON_RATIO * height);
Image armyImage = scale(readImage(ARMY_IMAGE), RESOURCE_ICON_RATIO * width, RESOURCE_ICON_RATIO * height);
LuaTable markers = mapData.getMarkers();
for (LuaValue key : markers.keys()) {
LuaTable markerData = markers.get(key).checktable();
switch (markerData.get("type").toString()) {
case "Mass":
addMarker(massImage, mapData, markerData, previewImage);
break;
case "Hydrocarbon":
addMarker(hydroImage, mapData, markerData, previewImage);
break;
case "Blank Marker":
if (!key.tojstring().startsWith(ARMY_PREFIX)) {
continue;
}
addMarker(armyImage, mapData, markerData, previewImage);
break;
}
}
}
private static void bgraToAbgr(byte[] buffer) {
for (int i = 0; i < buffer.length; i += 4) {
byte a = buffer[i + 3];
buffer[i + 3] = buffer[i + 2];
buffer[i + 2] = buffer[i + 1];
buffer[i + 1] = buffer[i];
buffer[i] = a;
}
}
private static BufferedImage readImage(String resource) throws IOException {
try (InputStream inputStream = PreviewGenerator.class.getResourceAsStream(resource)) {
return ImageIO.read(inputStream);
}
}
private static void addMarker(Image source, MapData mapData, LuaTable markerData, BufferedImage target) throws IOException {
LuaTable vector = markerData.get("position").checktable();
float x = vector.get(1).tofloat() / mapData.getWidth();
float y = vector.get(3).tofloat() / mapData.getHeight();
paintOnImage(source, x, y, target);
}
private static void paintOnImage(Image overlay, float xPercent, float yPercent, BufferedImage baseImage) {
int overlayWidth = overlay.getWidth(null);
int overlayHeight = overlay.getHeight(null);
int x = (int) (xPercent * baseImage.getWidth() - overlayWidth / 2);
int y = (int) (yPercent * baseImage.getHeight() - overlayHeight / 2);
x = Math.min(Math.max(0, x), baseImage.getWidth() - overlayWidth);
y = Math.min(Math.max(0, y), baseImage.getHeight() - overlayHeight);
baseImage.getGraphics().drawImage(overlay, x, y, null);
}
}