/**
* BetonQuest - advanced quests for Bukkit
* Copyright (C) 2016 Jakub "Co0sh" Sapalski
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package pl.betoncraft.betonquest.conversation;
import java.util.ArrayList;
import java.util.HashMap;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import pl.betoncraft.betonquest.BetonQuest;
import pl.betoncraft.betonquest.ConditionID;
import pl.betoncraft.betonquest.EventID;
import pl.betoncraft.betonquest.InstructionParseException;
import pl.betoncraft.betonquest.ObjectNotFoundException;
import pl.betoncraft.betonquest.config.Config;
import pl.betoncraft.betonquest.config.ConfigPackage;
import pl.betoncraft.betonquest.utils.Debug;
/**
* Represents the data of the conversation.
*
* @author Jakub Sapalski
*/
public class ConversationData {
private static ArrayList<String> externalPointers = new ArrayList<>();
private ConfigPackage pack;
private String convName;
private HashMap<String, String> quester = new HashMap<>(); // maps for multiple languages
private HashMap<String, String> prefix = new HashMap<>(); // global conversation prefix
private EventID[] finalEvents;
private String[] startingOptions;
private boolean blockMovement;
private String convIO;
private HashMap<String, Option> NPCOptions;
private HashMap<String, Option> playerOptions;
/**
* Loads conversation from package.
*
* @param pack
* the package containing this conversation
* @param name
* the name of the conversation
* @throws InstructionParseException
* when there is a syntax error in the defined conversation
*/
public ConversationData(ConfigPackage pack, String name) throws InstructionParseException {
this.pack = pack;
String pkg = pack.getName();
Debug.info(String.format("Loading %s conversation from %s package", name, pkg));
// package and name must be correct, it loads only existing
// conversations
convName = name;
// get the main data
FileConfiguration conv = pack.getConversation(name).getConfig();
if (conv.isConfigurationSection("quester")) {
for (String lang : conv.getConfigurationSection("quester").getKeys(false)) {
quester.put(lang, pack.getString("conversations." + name + ".quester." + lang));
}
} else {
quester.put(Config.getLanguage(), pack.getString("conversations." + name + ".quester"));
}
if (conv.isConfigurationSection("prefix")) {
for (String lang : conv.getConfigurationSection("prefix").getKeys(false)) {
String pref = pack.getString("conversations." + name + ".prefix." + lang);
if (pref != null && !pref.equals("")) {
prefix.put(lang, pref);
}
}
} else {
String pref = pack.getString("conversations." + name + ".prefix");
if (pref != null && !pref.equals("")) {
prefix.put(Config.getLanguage(), pref);
}
}
String rawFinalEvents = pack.getString("conversations." + name + ".final_events");
String rawStartingOptions = pack.getString("conversations." + name + ".first");
String stop = pack.getString("conversations." + name + ".stop");
blockMovement = stop != null && stop.equalsIgnoreCase("true");
convIO = pack.getString("conversations." + name + ".conversationIO");
if (convIO == null) {
convIO = BetonQuest.getInstance().getConfig().getString("default_conversation_IO");
}
// check if all data is valid (or at least exist)
if (BetonQuest.getInstance().getConvIO(convIO) == null) {
throw new InstructionParseException("Conversation IO " + convIO + " is not registered!");
}
if (quester == null || quester.isEmpty()) {
throw new InstructionParseException("Quester's name is not defined");
}
if (rawStartingOptions == null || rawStartingOptions.equals("")) {
throw new InstructionParseException("Starting options are not defined");
}
if (rawFinalEvents != null && !rawFinalEvents.equals("")) {
String[] array = rawFinalEvents.split(",");
finalEvents = new EventID[array.length];
for (int i = 0; i < array.length; i++) {
try {
finalEvents[i] = new EventID(pack, array[i]);
} catch (ObjectNotFoundException e) {
throw new InstructionParseException("Error while loading final events: " + e.getMessage());
}
}
} else {
finalEvents = new EventID[0];
}
// load all NPC options
ConfigurationSection NPCSection = pack.getConversation(name).getConfig().getConfigurationSection("NPC_options");
if (NPCSection == null) {
throw new InstructionParseException("NPC_options section not defined");
}
NPCOptions = new HashMap<>();
for (String key : NPCSection.getKeys(false)) {
NPCOptions.put(key, new NPCOption(key));
}
// check if all starting options point to existing NPC options
startingOptions = rawStartingOptions.split(",");
// remove spaces between the options
for (int i = 0; i < startingOptions.length; i++) {
startingOptions[i] = startingOptions[i].trim();
}
for (String startingOption : startingOptions) {
if (startingOption.contains(".")) {
String entirePointer = pack.getName() + "." + convName + ".<starting_option>."
+ startingOption;
externalPointers.add(entirePointer);
} else if (!NPCOptions.containsKey(startingOption)) {
throw new InstructionParseException("Starting option " + startingOption + " does not exist");
}
}
// load all Player options
ConfigurationSection playerSection = pack.getConversation(name).getConfig()
.getConfigurationSection("player_options");
playerOptions = new HashMap<>();
if (playerSection != null) {
for (String key : playerSection.getKeys(false)) {
playerOptions.put(key, new PlayerOption(key));
}
}
// check if every pointer points to existing option
for (Option option : NPCOptions.values()) {
for (String pointer : option.getPointers()) {
if (!playerOptions.containsKey(pointer)) {
throw new InstructionParseException(
String.format("NPC option %s points to %s player option, but it does not exist",
option.getName(), pointer));
}
}
}
for (Option option : playerOptions.values()) {
for (String pointer : option.getPointers()) {
if (pointer.contains(".")) {
String entirePointer = pack.getName() + "." + convName + "." + option.getName() + "." + pointer;
externalPointers.add(entirePointer);
} else if (!NPCOptions.containsKey(pointer)) {
throw new InstructionParseException(
String.format("Player option %s points to %s NPC option, but it does not exist",
option.getName(), pointer));
}
}
}
// done, everything will work
Debug.info(String.format("Conversation loaded: %d NPC options and %d player options", NPCOptions.size(),
playerOptions.size()));
}
/**
* @return the name of this conversation
*/
public String getName() {
return convName;
}
/**
* Gets the prefix of the conversation. If provided NPC option does not
* define one, the global one from the conversation is returned instead.
*
* @param lang
* language of the prefix
* @param option
* the quest starting npc option that defines the prefix of the
* conversation
* @return the conversation prefix, or null if not defined
*/
public String getPrefix(String lang, String option) {
// get prefix from an option
if (option != null) {
String pref = NPCOptions.get(option).getInlinePrefix(lang);
if (pref == null) {
pref = NPCOptions.get(option).getInlinePrefix(Config.getLanguage());
}
if (pref != null)
return pref;
}
// otherwise return global prefix
String global = prefix.get(lang);
if (global == null) {
global = prefix.get(Config.getLanguage());
}
return global;
}
/**
* @param lang
* language of quester's name
* @return the quester's name
*/
public String getQuester(String lang) {
String text = quester.get(lang);
if (text == null) {
text = quester.get(Config.getLanguage());
}
return text;
}
/**
* @return the final events
*/
public EventID[] getFinalEvents() {
return finalEvents;
}
/**
* @return the starting options
*/
public String[] getStartingOptions() {
return startingOptions;
}
/**
* @return true if movement should be blocked
*/
public boolean isMovementBlocked() {
return blockMovement;
}
/**
* @return the conversationIO
*/
public String getConversationIO() {
return convIO;
}
public String getText(String lang, String option, OptionType type) {
Option o = null;
if (type == OptionType.NPC) {
o = NPCOptions.get(option);
} else {
o = playerOptions.get(option);
}
if (o == null)
return null;
return o.getText(lang);
}
/**
* @return the name of the package
*/
public String getPackName() {
return pack.getName();
}
public ConditionID[] getConditionIDs(String option, OptionType type) {
HashMap<String, Option> options;
if (type == OptionType.NPC) {
options = NPCOptions;
} else {
options = playerOptions;
}
return options.get(option).getConditions();
}
public EventID[] getEventIDs(String option, OptionType type) {
HashMap<String, Option> options;
if (type == OptionType.NPC) {
options = NPCOptions;
} else {
options = playerOptions;
}
return options.get(option).getEvents();
}
public String[] getPointers(String option, OptionType type) {
HashMap<String, Option> options;
if (type == OptionType.NPC) {
options = NPCOptions;
} else {
options = playerOptions;
}
return options.get(option).getPointers();
}
/**
* Checks if external pointers point to valid options. It cannot be checked
* when constructing ConversationData objects because conversations that are
* being pointed to may not yet exist.
*
* This method should be called when all conversations are loaded. It will
* not throw any exceptions, just display errors in the console.
*/
public static void postEnableCheck() {
for (String externalPointer : externalPointers) {
String[] parts = externalPointer.split("\\.");
String packName = parts[0];
String sourceConv = parts[1];
String sourceOption = parts[2];
String targetConv = parts[3];
String targetOption = parts[4];
ConversationData conv = BetonQuest.getInstance().getConversation(packName + "." + targetConv);
if (conv == null) {
Debug.error("External pointer in '" + packName + "' package, '" + sourceConv + "' conversation, "
+ ((sourceOption.equals("<starting_option>")) ? "starting option"
: ("'" + sourceOption + "' player option"))
+ " points to '" + targetConv
+ "' conversation, but it does not even exist. Check your spelling!");
continue;
}
if (conv.getText(Config.getLanguage(), targetOption, OptionType.NPC) == null) {
Debug.error("External pointer in '" + packName + "' package, '" + sourceConv + "' conversation, "
+ ((sourceOption.equals("<starting_option>")) ? "starting option"
: ("'" + sourceOption + "' player option"))
+ " points to '" + targetOption + "' NPC option in '" + targetConv
+ "' conversation, but it does not exist.");
}
}
externalPointers.clear();
}
/**
* Represents an option
*/
private abstract class Option {
private String name;
private HashMap<String, String> inlinePrefix = new HashMap<>();
private HashMap<String, String> text = new HashMap<>();
private ConditionID[] conditions;
private EventID[] events;
private String[] pointers;
public Option(String name, String type, String visibleType) throws InstructionParseException {
this.name = name;
String defaultLang = Config.getLanguage();
FileConfiguration conv = pack.getConversation(convName).getConfig();
if (conv.isConfigurationSection(type + "." + name + ".prefix")) {
for (String lang : conv.getConfigurationSection(type + "." + name + ".prefix").getKeys(false)) {
String pref = pack .getString("conversations." + convName + "." + type + "." + name + ".prefix."
+ lang);
if (pref != null && !pref.equals("")) {
inlinePrefix.put(lang, pref);
}
}
if (!inlinePrefix.containsKey(defaultLang)) {
throw new InstructionParseException("No default language for " + name + " " + visibleType
+ " prefix");
}
} else {
String pref = pack.getString("conversations." + convName + "." + type + "." + name + ".prefix");
if (pref != null && !pref.equals("")) {
inlinePrefix.put(defaultLang, pref);
}
}
if (conv.isConfigurationSection(type + "." + name + ".text")) {
for (String lang : conv.getConfigurationSection(type + "." + name + ".text").getKeys(false)) {
text.put(lang, pack.getString("conversations." + convName + "." + type + "." + name + ".text."
+ lang).replace("\\n", "\n"));
}
if (!text.containsKey(defaultLang)) {
throw new InstructionParseException("No default language for " + name + " " + visibleType);
}
} else {
text.put(defaultLang, pack.getString("conversations." + convName + "." + type + "." + name + ".text")
.replace("\\n", "\n"));
}
ArrayList<String> variables = new ArrayList<>();
for (String theText : text.values()) {
if (theText == null || theText.equals(""))
throw new InstructionParseException("Text not defined in " + visibleType + " " + name);
// variables are possibly duplicated because there probably is
// the same variable in every language
ArrayList<String> possiblyDuplicatedVariables = BetonQuest.resolveVariables(theText);
for (String possiblyDuplicatedVariable : possiblyDuplicatedVariables) {
if (variables.contains(possiblyDuplicatedVariable))
continue;
variables.add(possiblyDuplicatedVariable);
}
}
for (String variable : variables) {
try {
BetonQuest.createVariable(pack, variable);
} catch (InstructionParseException e) {
throw new InstructionParseException("Error while creating '" + variable + "' variable: "
+ e.getMessage());
}
}
String rawConditions = pack
.getString("conversations." + convName + "." + type + "." + name + ".conditions");
String[] cond1 = new String[] {};
if (rawConditions != null && !rawConditions.equals("")) {
cond1 = rawConditions.split(",");
}
String rawCondition = pack.getString("conversations." + convName + "." + type + "." + name + ".condition");
String[] cond2 = new String[] {};
if (rawCondition != null && !rawCondition.equals("")) {
cond2 = rawCondition.split(",");
}
conditions = new ConditionID[cond1.length + cond2.length];
int count = 0;
try {
for (String cond : cond1) {
conditions[count] = new ConditionID(pack, cond.trim());
count++;
}
for (String cond : cond2) {
conditions[count] = new ConditionID(pack, cond.trim());
count++;
}
} catch (ObjectNotFoundException e) {
throw new InstructionParseException("Error in '" + name + "' " + visibleType + " option's conditions: "
+ e.getMessage());
}
String rawEvents = pack.getString("conversations." + convName + "." + type + "." + name + ".events");
String[] event1 = new String[] {};
if (rawEvents != null && !rawEvents.equals("")) {
event1 = rawEvents.split(",");
}
String rawEvent = pack.getString("conversations." + convName + "." + type + "." + name + ".event");
String[] event2 = new String[] {};
if (rawEvent != null && !rawEvent.equals("")) {
event2 = rawEvent.split(",");
}
events = new EventID[event1.length + event2.length];
count = 0;
try {
for (String event : event1) {
events[count] = new EventID(pack, event.trim());
count++;
}
for (String event : event2) {
events[count] = new EventID(pack, event.trim());
count++;
}
} catch (ObjectNotFoundException e) {
throw new InstructionParseException("Error in '" + name + "' " + visibleType + " option's events: "
+ e.getMessage());
}
String rawPointers = pack.getString("conversations." + convName + "." + type + "." + name + ".pointers");
String[] pointer1 = new String[] {};
if (rawPointers != null && !rawPointers.equals("")) {
pointer1 = rawPointers.split(",");
}
String rawPointer = pack.getString("conversations." + convName + "." + type + "." + name + ".pointer");
String[] pointer2 = new String[] {};
if (rawPointer != null && !rawPointer.equals("")) {
pointer2 = rawPointer.split(",");
}
pointers = new String[pointer1.length + pointer2.length];
count = 0;
for (String pointer : pointer1) {
pointers[count] = pointer.trim();
count++;
}
for (String pointer : pointer2) {
pointers[count] = pointer.trim();
count++;
}
}
public String getName() {
return name;
}
public String getInlinePrefix(String lang) {
String thePrefix = inlinePrefix.get(lang);
if (thePrefix == null) {
thePrefix = inlinePrefix.get(Config.getLanguage());
}
return thePrefix;
}
public String getText(String lang) {
String theText = text.get(lang);
if (theText == null) {
theText = text.get(Config.getLanguage());
}
return theText;
}
public ConditionID[] getConditions() {
return conditions;
}
public EventID[] getEvents() {
return events;
}
public String[] getPointers() {
return pointers;
}
}
/**
* Represents an option which can be choosen by the Player
*/
private class PlayerOption extends Option {
public PlayerOption(String name) throws InstructionParseException {
super(name, "player_options", "player option");
}
}
/**
* Represents an option which can be choosen by the NPC
*/
private class NPCOption extends Option {
public NPCOption(String name) throws InstructionParseException {
super(name, "NPC_options", "NPC option");
}
}
public static enum OptionType {
NPC, PLAYER
}
}