/*
* 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.easyquest;
import com.mxgraph.io.mxCodec;
import com.mxgraph.io.mxCodecRegistry;
import com.mxgraph.io.mxObjectCodec;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.model.mxICell;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.view.mxGraph;
import illarion.easyquest.quest.*;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.annotation.Nonnull;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
/**
* This is the input/output class for the quests. It handles loading and saving quest graphs. Also it handles
* creating the LUA quest files.
*
* @author Martin Karing <nitram@illarion.org>
*/
public final class QuestIO {
/**
* The character sets that will be tried to load the file. One by one. The first one in the list is
* {@link #CHARSET} as this one is the most likely one to be the one used.
*/
@Nonnull
private static final Collection<Charset> CHARSETS;
/**
* The char set that is used by default to load and save the data.
*/
public static final Charset CHARSET;
static {
CHARSET = Charset.forName("ISO-8859-1");
CHARSETS = new ArrayList<>();
CHARSETS.add(CHARSET);
CHARSETS.addAll(Charset.availableCharsets().values());
mxCodecRegistry.register(new mxObjectCodec(new Handler()));
mxCodecRegistry.addPackage(Handler.class.getPackage().getName());
mxCodecRegistry.register(new mxObjectCodec(new Status()));
mxCodecRegistry.addPackage(Status.class.getPackage().getName());
mxCodecRegistry.register(new mxObjectCodec(new Trigger()));
mxCodecRegistry.addPackage(Trigger.class.getPackage().getName());
mxCodecRegistry.register(new mxObjectCodec(new Position()));
mxCodecRegistry.addPackage(Position.class.getPackage().getName());
}
private QuestIO() {
}
/**
* Load the graph model of a quest from a file. This is the quest data as its handled internally by the easyQuest
* editor.
*
* @param file the file that is used as data source
* @return the graph model instance containing the data of the file
* @throws IOException in case reading the model from the file fails for any reason
*/
@Nonnull
public static mxIGraphModel loadGraphModel(@Nonnull Path file) throws IOException {
if (!Files.isReadable(file)) {
throw new IOException("Can't read the required file.");
}
IOException firstException = null;
for (Charset charset : CHARSETS) {
try (Reader reader = Files.newBufferedReader(file, charset)) {
return loadGraphModel(reader);
} catch (IOException e) {
if (firstException == null) {
firstException = e;
}
}
}
throw firstException;
}
@Nonnull
public static mxIGraphModel loadGraphModel(@Nonnull Reader reader) throws IOException {
try {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
Document document = docBuilder.parse(new InputSource(reader));
mxCodec codec = new mxCodec(document);
mxIGraphModel model = new mxGraphModel();
codec.decode(document.getDocumentElement(), model);
return model;
} catch (ParserConfigurationException | SAXException e) {
throw new IOException(e);
}
}
/**
* Save a graph model to the file system.
*
* @param model the model to store to the file system
* @param target tje target in the file system that will receive the data
* @throws IOException in case saving the file fails
*/
public static void saveGraphModel(@Nonnull mxIGraphModel model, @Nonnull Path target)
throws IOException {
if (!Files.isWritable(target)) {
throw new IOException("Can't write the required file.");
}
mxCodec codec = new mxCodec();
Node node = codec.encode(model);
if (node == null) {
throw new IOException("Model can't be encoded to XML.");
}
try (Writer writer = Files.newBufferedWriter(target, CHARSET)) {
Transformer tf = TransformerFactory.newInstance().newTransformer();
tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
tf.setOutputProperty(OutputKeys.ENCODING, CHARSET.name());
tf.transform(new DOMSource(node), new StreamResult(writer));
writer.flush();
} catch (TransformerException e) {
throw new IOException(e);
}
}
/**
* Export a quest to its lua files.
*
* @param model the quest model
* @param rootDirectory the directory to store the root directory in
* @throws IOException in case anything goes wrong
*/
public static void exportQuest(@Nonnull mxIGraphModel model, @Nonnull Path rootDirectory)
throws IOException {
if (Files.isDirectory(rootDirectory)) {
Files.walkFileTree(rootDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc == null) {
Files.delete(dir);
return CONTINUE;
} else {
throw exc;
}
}
});
}
Files.createDirectories(rootDirectory);
String questName = rootDirectory.getName(rootDirectory.getNameCount() - 1).toString();
mxICell root = (mxCell) model.getRoot();
Object idNode = root.getValue();
int questID = -1;
if (idNode != null) {
try {
questID = Integer.parseInt(idNode.toString());
} catch (NumberFormatException ignored) {
}
}
if (questID == -1) {
throw new IOException("Required quest ID is not set.");
}
Path questMainFile = rootDirectory.resolve("quest.txt");
if (Files.exists(questMainFile)) {
Files.delete(questMainFile);
}
mxGraph graph = new mxGraph(model);
Object[] edges = graph.getChildEdges(graph.getDefaultParent());
for (int i = 0; i < edges.length; i++) {
mxCell edge = (mxCell) edges[i];
Trigger trigger = (Trigger) edge.getValue();
TriggerTemplate template = TriggerTemplates.getInstance().getTemplate(trigger.getType());
String scriptName = "trigger" + Integer.toString(i + 1);
mxICell source = edge.getSource();
mxICell target = edge.getTarget();
Status sourceState = (Status) source.getValue();
Status targetState = (Status) target.getValue();
String sourceId = sourceState.isStart() ? "0" : source.getId();
String targetId = targetState.isStart() ? "0" : target.getId();
Object[] parameters = trigger.getParameters();
Handler[] handlers = targetState.getHandlers();
Collection<String> handlerTypes = new HashSet<>();
Condition[] conditions = trigger.getConditions();
StringBuilder handlerCode = new StringBuilder();
if (handlers != null) {
for (Handler handler : handlers) {
String type = handler.getType();
Object[] handlerParameters = handler.getParameters();
HandlerTemplate handlerTemplate = HandlerTemplates.getInstance().getTemplate(type);
int playerIndex = handlerTemplate.getPlayerIndex();
handlerTypes.add(type);
handlerCode.append(" handler.").append(type.toLowerCase()).append('.').append(type).append('(');
if (handlerParameters.length > 0) {
if (playerIndex == 0) {
handlerCode.append("PLAYER, ");
}
handlerCode.append(exportParameter(handlerParameters[0],
handlerTemplate.getParameter(0).getType()));
for (int j = 1; j < handlerParameters.length; ++j) {
if (playerIndex == j) {
handlerCode.append(", PLAYER");
}
handlerCode.append(", ").append(exportParameter(handlerParameters[j],
handlerTemplate.getParameter(j).getType()));
}
}
handlerCode.append("):execute()\n");
}
}
StringBuilder conditionCode = new StringBuilder();
if (conditions != null) {
for (Condition condition : conditions) {
String type = condition.getType();
Object[] conditionParameters = condition.getParameters();
ConditionTemplate conditionTemplate = ConditionTemplates.getInstance().getTemplate(type);
String conditionString = conditionTemplate.getCondition();
if (conditionString != null) {
for (int j = 0; j < conditionParameters.length; ++j) {
Object param = conditionParameters[j];
String paramName = conditionTemplate.getParameter(j).getName();
String paramType = conditionTemplate.getParameter(j).getType();
String operator = null;
String value = null;
if ("INTEGERRELATION".equals(paramType)) {
IntegerRelation ir = (IntegerRelation) param;
value = String.valueOf(ir.getInteger());
operator = ir.getRelation().toLua();
}
if (operator != null) {
conditionString = conditionString.replaceAll("OPERATOR_" + j, operator)
.replaceAll(paramName, value);
}
}
}
if (conditionCode.length() > 0) {
conditionCode.append(" and ");
}
conditionCode.append(conditionString).append('\n');
}
}
if (conditionCode.length() == 0) {
conditionCode.append("true\n");
}
try (BufferedWriter writer = Files.newBufferedWriter(rootDirectory.resolve(scriptName + ".lua"), CHARSET)) {
for (String type : handlerTypes) {
writer.write("require(\"handler.");
writer.write(type.toLowerCase());
writer.write("\"}");
writer.newLine();
}
String header = template.getHeader();
if (header != null) {
writer.write(header);
writer.newLine();
}
writer.write("module(\"questsystem.");
writer.write(questName);
writer.write('.');
writer.write(scriptName);
writer.write("\", package.seeall)");
writer.newLine();
writer.write("local QUEST_NUMBER = ");
writer.write(Integer.toString(questID));
writer.newLine();
writer.write("local PRECONDITION_QUESTSTATE = ");
writer.write(sourceId);
writer.newLine();
writer.write("local POSTCONDITION_QUESTSTATE = ");
writer.write(targetId);
writer.newLine();
int paramCount = template.size();
if (parameters == null || paramCount != parameters.length) {
throw new IOException("Required parameters are not present.");
}
for (int j = 0; j < template.size(); ++j) {
writer.write("local ");
writer.write(template.getParameter(j).getName());
writer.write(" = ");
writer.write(exportParameter(parameters[j], template.getParameter(j).getType()));
writer.newLine();
}
String body = template.getBody();
if (body != null) {
writer.write(body);
writer.newLine();
}
writer.write("function HANDLER(PLAYER)");
writer.newLine();
writer.write(handlerCode.toString());
writer.write("end");
writer.newLine();
writer.write("function ADDITIONALCONDITIONS(PLAYER)");
writer.newLine();
writer.write("return ");
writer.write(conditionCode.toString());
writer.write("end");
writer.flush();
}
try (BufferedWriter writer = Files
.newBufferedWriter(rootDirectory.resolve("quest.txt"), CHARSET, APPEND, CREATE)) {
String cat = template.getCategory();
TemplateParameter id = template.getId();
String type = id == null ? null : id.getType();
String entryPoint = template.getEntryPoint();
if (cat == null || type == null || entryPoint == null) {
throw new IOException("Template appears to be incomplete.");
}
writer.write(template.getCategory());
writer.write(',');
writer.write(exportId(trigger.getObjectId(), template.getId().getType()));
writer.write(',');
writer.write(template.getEntryPoint());
writer.write(',');
writer.write(scriptName);
writer.newLine();
writer.flush();
}
}
}
private static String exportId(Object parameter, String type) {
if ("POSITION".equals(type)) {
Position p = (Position) parameter;
return p.getX() + "," + p.getY() + ',' + p.getZ();
}
if ("INTEGER".equals(type)) {
if (parameter instanceof Long) {
Long n = (Long) parameter;
return n.toString();
}
return parameter.toString();
}
return "TYPE NOT SUPPORTED";
}
private static String exportParameter(@Nonnull Object parameter, @Nonnull String type) {
if ("TEXT".equals(type)) {
String s = (String) parameter;
return '"' + s.replace("\\", "\\\\").replace("\"", "\\\"") + '"';
}
if ("POSITION".equals(type)) {
Position p = (Position) parameter;
return "position(" + p.getX() + ", " + p.getY() + ", " + p.getZ() + ')';
}
if ("INTEGER".equals(type)) {
if (parameter instanceof Long) {
Long n = (Long) parameter;
return n.toString();
}
return parameter.toString();
}
return "TYPE NOT SUPPORTED";
}
}