/* * 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.easynpc.writer; import illarion.common.util.CopyrightHeader; import illarion.easynpc.ParsedNpc; import illarion.easynpc.Parser; import illarion.easynpc.data.CharacterLanguage; import javax.annotation.Nonnull; import java.io.IOException; import java.io.Writer; import java.util.Collection; /** * This is the LUA writer. It writes the data supplied by a parsed NPC to a lua script. * * @author Martin Karing <nitram@illarion.org> */ public final class LuaWriter { /** * The stages of the writing of the script. */ public enum WritingStage { /** * Clothes writing stage. Everything needed to setup the initialization of the equipment of the NPC should be * written here. */ Clothes, /** * The writing stage for the guard part of NPCs. */ Guarding, /** * Header writing stage. This lines appears in the head of the LUA script. */ Header, /** * Talking writing stage. All things related to the initialization of the speech of the NPC should be written * here. */ Talking, /** * Trading writing stage. All things related to the implementation of the trader functions should be written * here. */ Trading } /** * The new line string that is used by default for this scripts. */ public static final String NL = "\n"; /** * The code used to add a new language the NPC is able to speak. */ private static final String addLanguageCode = "mainNPC:addLanguage(%1$s)"; /** * The code used to set the auto introduction mode. */ private static final String addSetAutoIntro = "mainNPC:setAutoIntroduceMode(%1$s)"; /** * The comment separator line used in the comments. */ private static final String commentSepLine = "--------------------------------------------------------------------------------"; /** * The LUA code needed to set the message displayed in case a character is * talking in the wrong language. */ private static final String setConfusedMessageCode = "mainNPC:setConfusedMessage(\"%1$s\", \"%2$s\")"; /** * The LUA code needed to set the default language of an NPC. */ private static final String setDefaultLanguageCode = "mainNPC:setDefaultLanguage(%1$s)"; /** * The LUA code needed to set the message displayed when the player performs * a look at on the NPC. */ private static final String setLookatMessageCode = "mainNPC:setLookat(\"%1$s\", \"%2$s\")"; /** * The LUA code needed to set the affiliation of a NPC. */ private static final String setAffiliationCode = "mainNPC:setAffiliation(%1$s)"; /** * The LUA code needed to set the message displayed in case the player uses * the NPC. */ private static final String setUseMessageCode = "mainNPC:setUseMessage(\"%1$s\", \"%2$s\")"; /** * The copyright header of the LUA writer. */ @Nonnull private static final CopyrightHeader COPYRIGHT_HEADER = new CopyrightHeader(80, "--[[", "]]", null, null); /** * Private default constructor to ensure the only instance to be the * singleton instance. */ private LuaWriter() { // nothing to do here } /** * Main writing method. This method causes that the NPC is written to a LUA * script. * * @param source the parsed NPC that acts as data source for the writer * @param target the writer that takes the written data * @throws IOException thrown in case a writing operation failed */ public static void write( @Nonnull ParsedNpc source, @Nonnull Writer target, boolean generated) throws IOException { LuaRequireTable requires = new LuaRequireTable(); // first write the header with some basic information in the comment if (!generated) { writeIntro(source, requires, target, WritingStage.Header); } // now following the modules writeModules(source, requires, target); // now the big bad initialization introInitPart(target, requires); writeNpcAffiliation(source, target); boolean talkingExists = checkStageExists(source, WritingStage.Talking); boolean tradingExists = checkStageExists(source, WritingStage.Trading); boolean guardingExists = checkStageExists(source, WritingStage.Guarding); if (talkingExists) { writeIntro(source, requires, target, WritingStage.Talking); } if (tradingExists) { writeIntro(source, requires, target, WritingStage.Trading); } if (guardingExists) { writeIntro(source, requires, target, WritingStage.Guarding); } if (talkingExists) { writeStage(source, target, requires, WritingStage.Talking); } if (tradingExists) { writeStage(source, target, requires, WritingStage.Trading); } if (guardingExists) { writeStage(source, target, requires, WritingStage.Guarding); } writeNpcLanguages(source, target); writeNpcSpecialMessages(source, target); if (checkStageExists(source, WritingStage.Clothes)) { writeIntro(source, requires, target, WritingStage.Clothes); writeStage(source, target, requires, WritingStage.Clothes); } autoIntroPart(source, target); outroInitPart(target); // init part is done // write the actual NPC script writeMainScript(target); // finish the script. Nothing is to be written after this line writeLastLines(target); } /** * Write the LUA code needed to set the auto introduction mode of the NPC. * * @param source the NPC that is the data source for this function * @param target the writer that is the target of the function * @throws IOException thrown in case the writing operation fails */ private static void autoIntroPart(@Nonnull ParsedNpc source, @Nonnull Writer target) throws IOException { target.write(String.format(addSetAutoIntro, source.getAutoIntroduce() ? "true" : "false")); writeNewLine(target); } /** * Fill the SQL query builder will all required data. * * @param source the NPC that is the data source to build the query * @return the prepared SQL query */ private static String buildSQL(@Nonnull ParsedNpc source) { int count = source.getDataCount(); SQLBuilder builder = new SQLBuilder(); for (int i = 0; i < count; ++i) { LuaWritable writable = source.getLuaData(i); writable.buildSQL(builder); } builder.setNpcName(source.getNpcName()); builder.setNpcScript("npc." + source.getModuleName()); builder.setNpcFaceTo(source.getNpcDir().getId()); builder.setNpcPosX(source.getNpcPos().getX()); builder.setNpcPosY(source.getNpcPos().getY()); builder.setNpcPosZ(source.getNpcPos().getZ()); builder.setNpcType(source.getNpcRace().getId()); builder.setNpcSex(source.getNpcSex().getId()); return builder.getSQL(); } /** * Check if there are any entries in this stage. * * @param source the NPC that is the data source * @param stage the stage to check * @return {@code true} in case the NPC contains entries in this stage. */ private static boolean checkStageExists(@Nonnull ParsedNpc source, @Nonnull WritingStage stage) { int count = source.getDataCount(); for (int i = 0; i < count; ++i) { LuaWritable writable = source.getLuaData(i); if (writable.effectsLuaWritingStage(stage)) { return true; } } return false; } private static void writeNewLine(@Nonnull Writer target) throws IOException { target.write(NL); } /** * Write the lines needed before the initialization part of the NPC. * * @param target the writer that receives the text written in this function * @throws IOException the exception thrown in case the writing functions * fail */ private static void introInitPart(@Nonnull Writer target, @Nonnull LuaRequireTable requires) throws IOException { target.write("local function initNpc()"); writeNewLine(target); target.write("local mainNPC = " + requires.getStorage("npc.base.basic") + "()"); writeNewLine(target); } /** * Write the end of the initialization part. * * @param target the writer that receives the text written in this function * @throws IOException thrown in case the writing functions fail */ private static void outroInitPart(@Nonnull Writer target) throws IOException { writeNewLine(target); target.write("mainNPC:initDone()"); writeNewLine(target); target.write("return mainNPC"); writeNewLine(target); target.write("end"); writeNewLine(target); } /** * Write the introduction texts for the easyNPC script. * * @param source the parsed NPC that is the data source * @param target the writer that is the target * @param stage the current stage that is supposed to be processed * @throws IOException thrown in case the writing operations fail */ private static void writeIntro( @Nonnull ParsedNpc source, @Nonnull LuaRequireTable requires, @Nonnull Writer target, @Nonnull WritingStage stage) throws IOException { switch (stage) { case Header: COPYRIGHT_HEADER.writeTo(target); target.write(commentSepLine); writeNewLine(target); target.write(String.format("-- %1$-10s%2$-49s%3$15s --%n", "NPC Name:", source.getNpcName(), source.getAffiliation().name())); target.write(String.format("-- %1$-10s%2$-64s --%n", "NPC Job:", source.getJob())); String freeLine = String.format("-- %1$74s --%n", ""); target.write(freeLine); String positionString = Integer.toString(source.getNpcPos().getX()) + ", " + Integer.toString(source.getNpcPos().getY()) + ", " + Integer.toString(source.getNpcPos().getZ()); target.write(String.format("-- %1$-37s%2$-37s --", "NPC Race: " + source.getNpcRace().name(), "NPC Position: " + positionString)); writeNewLine(target); target.write(String.format("-- %1$-37s%2$-37s --", "NPC Sex: " + source.getNpcSex().name(), "NPC Direction: " + source.getNpcDir().name())); writeNewLine(target); target.write(freeLine); Collection<String> authors = source.getAuthors(); String authorFormat = "-- %1$-10s%2$-64s --%n"; if (authors.isEmpty()) { target.write(String.format(authorFormat, "Author:", "not set")); } else { String authorText = "Author:"; for (String author : authors) { target.write(String.format(authorFormat, authorText, author)); authorText = ""; } } target.write(String.format("-- %1$-47s%2$27s --%n", "", Parser.APPLICATION.getApplicationIdentifier())); target.write(commentSepLine); writeNewLine(target); writeNewLine(target); target.write("--[[SQL"); writeNewLine(target); target.write(buildSQL(source)); writeNewLine(target); target.write("---]]"); writeNewLine(target); writeNewLine(target); break; case Talking: target.write("local talkingNPC = " + requires.getStorage("npc.base.talk") + "(mainNPC)"); writeNewLine(target); break; case Trading: target.write("local tradingNPC = " + requires.getStorage("npc.base.trade") + "(mainNPC)"); writeNewLine(target); break; case Guarding: target.write("local guardNPC = " + requires.getStorage("npc.base.guard") + "(mainNPC)"); writeNewLine(target); break; case Clothes: break; } } /** * Write the very last lines of the script. Nothing should follow after this * lines. It triggers the initialization and clean the script up. * * @param target the writer that receives the text written by this function * @throws IOException thrown in case the writing operations fail */ private static void writeLastLines(@Nonnull Writer target) throws IOException { target.write("return M"); writeNewLine(target); } /** * Write the actual NPC Script that does the work. * * @param target the writer that receives the text output * @throws IOException thrown in case the writing operation fails */ private static void writeMainScript(@Nonnull Writer target) throws IOException { target.write("local mNPC = initNpc()"); writeNewLine(target); target.write("initNpc = nil"); writeNewLine(target); target.write("local M = {}"); writeNewLine(target); target.write("function M.receiveText(npcChar, texttype, message, speaker) "); target.write("mNPC:receiveText(npcChar, texttype, speaker, message) "); target.write("end"); writeNewLine(target); target.write("function M.nextCycle(npcChar) "); target.write("mNPC:nextCycle(npcChar) "); target.write("end"); writeNewLine(target); target.write("function M.lookAtNpc(npcChar, char, mode) "); target.write("mNPC:lookAt(npcChar, char, mode) "); target.write("end"); writeNewLine(target); target.write("function M.useNPC(npcChar, char, counter, param) "); target.write("mNPC:use(npcChar, char) "); target.write("end"); writeNewLine(target); } /** * Collect the required modules from all elements that are written to the * script, sort those modules and write them after into the needed form for * the LUA script. * * @param source the parsed NPC that supplies the data * @param target the target writer that receives the written text * @throws IOException thrown in case the writing operations fail */ private static <T extends LuaWritable> void writeModules( @Nonnull Iterable<T> source, @Nonnull LuaRequireTable requires, @Nonnull Writer target) throws IOException { for (LuaWritable writable : source) { writable.getRequiredModules().forEach(requires::registerDependency); } requires.registerDependency("npc.base.basic"); requires.writeDependencies(target); } /** * Write the code needed to set the possible languages and the default * language of the NPC. * * @param source the parsed NPC that supplies the required data * @param target the writer that receives the text from this function * @throws IOException thrown in case the writing operations fail */ private static void writeNpcLanguages(@Nonnull ParsedNpc source, @Nonnull Writer target) throws IOException { CharacterLanguage[] languages = source.getLanguages(); for (CharacterLanguage lang : languages) { target.write(String.format(addLanguageCode, Integer.toString(lang.getLangId()))); writeNewLine(target); } target.write(String.format(setDefaultLanguageCode, Integer.toString(source.getDefaultLanguage().getLangId()))); writeNewLine(target); } /** * Write the code needed to set the special messages each NPC is able to * handle. * * @param source the parsed NPC that is the data source * @param target the writer that receives the written text * @throws IOException thrown in case the writing operations fail */ private static void writeNpcSpecialMessages( @Nonnull ParsedNpc source, @Nonnull Writer target) throws IOException { target.write(String.format(setLookatMessageCode, source.getGermanLookat(), source.getEnglishLookat())); writeNewLine(target); target.write(String.format(setUseMessageCode, source.getGermanUse(), source.getEnglishUse())); writeNewLine(target); target.write(String.format(setConfusedMessageCode, source.getGermanWrongLang(), source.getEnglishWrongLang())); writeNewLine(target); } /** * Write the code needed to set the special messages each NPC is able to * handle. * * @param source the parsed NPC that is the data source * @param target the writer that receives the written text * @throws IOException thrown in case the writing operations fail */ private static void writeNpcAffiliation( @Nonnull ParsedNpc source, @Nonnull Writer target) throws IOException { target.write(String.format(setAffiliationCode, source.getAffiliation().getFactionId())); writeNewLine(target); } /** * Write a given stage of the full NPC data. * * @param source the parsed NPC that is the data source * @param target the writer that is the target * @param stage the current stage that is supposed to be processed * @throws IOException thrown in case the writing operations fail */ private static <T extends LuaWritable> void writeStage( @Nonnull Iterable<T> source, @Nonnull Writer target, @Nonnull LuaRequireTable requires, @Nonnull WritingStage stage) throws IOException { for (LuaWritable writable : source) { if (writable.effectsLuaWritingStage(stage)) { writable.writeLua(target, requires, stage); } } } }