pl.betoncraft.betonquest.editor.model.QuestPackage.java Source code

Java tutorial

Introduction

Here is the source code for pl.betoncraft.betonquest.editor.model.QuestPackage.java

Source

/**
 * BetonQuest Editor - advanced quest creating tool for BetonQuest
 * Copyright (C) 2015  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.editor.model;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map.Entry;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import pl.betoncraft.betonquest.editor.controller.ConversationController;
import pl.betoncraft.betonquest.editor.controller.ExceptionController;
import pl.betoncraft.betonquest.editor.controller.NameEditController;
import pl.betoncraft.betonquest.editor.data.ConditionWrapper;
import pl.betoncraft.betonquest.editor.data.Editable;
import pl.betoncraft.betonquest.editor.data.ID;
import pl.betoncraft.betonquest.editor.data.IdWrapper;
import pl.betoncraft.betonquest.editor.data.TranslatableText;

/**
 * Keeps all data about the quest package.
 *
 * @author Jakub Sapalski
 */
public class QuestPackage implements Editable {

    private final StringProperty packName;
    private final PackageSet set;
    private String defLang;
    private final HashMap<String, Integer> languages = new HashMap<>();
    private final ObservableList<Conversation> conversations = FXCollections.observableArrayList();
    private final ObservableList<Event> events = FXCollections.observableArrayList();
    private final ObservableList<Condition> conditions = FXCollections.observableArrayList();
    private final ObservableList<Objective> objectives = FXCollections.observableArrayList();
    private final ObservableList<JournalEntry> journal = FXCollections.observableArrayList();
    private final ObservableList<Item> items = FXCollections.observableArrayList();
    private final ObservableList<GlobalVariable> variables = FXCollections.observableArrayList();
    private final ObservableList<GlobalLocation> locations = FXCollections.observableArrayList();
    private final ObservableList<StaticEvent> staticEvents = FXCollections.observableArrayList();
    private final ObservableList<QuestCanceler> cancelers = FXCollections.observableArrayList();
    private final ObservableList<NpcBinding> npcBindings = FXCollections.observableArrayList();
    private final ObservableList<MainPageLine> mainPage = FXCollections.observableArrayList();
    private final ObservableList<Tag> tags = FXCollections.observableArrayList();
    private final ObservableList<PointCategory> points = FXCollections.observableArrayList();

    /**
     * Loads a package using a hashmap containing all data. The key is a file
     * name, the value is a hashmap containing keys and values of that YAML
     * file.
     * 
     * @param map
     */
    public QuestPackage(PackageSet set, String id, HashMap<String, LinkedHashMap<String, String>> data) {
        this.set = set;
        packName = new SimpleStringProperty(id);
        try {
            // handling journal.yml
            HashMap<String, String> journalMap = data.get("journal");
            if (journalMap != null) {
                int journalIndex = 0;
                for (Entry<String, String> entry : journalMap.entrySet()) {
                    String value = entry.getValue();
                    String[] parts = entry.getKey().split("\\.");
                    // getting the right entry
                    JournalEntry journalEntry = newByID(parts[0], name -> new JournalEntry(this, name));
                    if (journalEntry.getIndex() < 0)
                        journalEntry.setIndex(journalIndex++);
                    // handling entry data
                    if (parts.length > 1) {
                        String lang = parts[1];
                        if (!languages.containsKey(lang)) {
                            languages.put(lang, 1);
                        } else {
                            languages.put(lang, languages.get(lang) + 1);
                        }
                        journalEntry.getText().addLang(lang, value);
                    } else {
                        journalEntry.getText().setDef(value);
                    }
                }
            }
            // handling items.yml
            HashMap<String, String> itemsMap = data.get("items");
            if (itemsMap != null) {
                int itemIndex = 0;
                for (String key : itemsMap.keySet()) {
                    Item item = newByID(key, name -> new Item(this, name));
                    item.getInstruction().set(itemsMap.get(key));
                    if (item.getIndex() < 0)
                        item.setIndex(itemIndex++);
                }
            }
            // handling conditions.yml
            HashMap<String, String> conditionsMap = data.get("conditions");
            if (conditionsMap != null) {
                int conditionIndex = 0;
                for (String key : conditionsMap.keySet()) {
                    Condition condition = newByID(key, name -> new Condition(this, name));
                    condition.getInstruction().set(conditionsMap.get(key));
                    if (condition.getIndex() < 0)
                        condition.setIndex(conditionIndex++);
                }
            }
            // handling event.yml
            HashMap<String, String> eventsMap = data.get("events");
            if (eventsMap != null) {
                int eventIndex = 0;
                for (String key : eventsMap.keySet()) {
                    Event event = newByID(key, name -> new Event(this, name));
                    event.getInstruction().set(eventsMap.get(key));
                    if (event.getIndex() < 0)
                        event.setIndex(eventIndex++);
                }
            }
            // handling objectives.yml
            HashMap<String, String> objectivesMap = data.get("objectives");
            if (objectivesMap != null) {
                int objectiveIndex = 0;
                for (String key : objectivesMap.keySet()) {
                    Objective objective = newByID(key, name -> new Objective(this, name));
                    objective.getInstruction().set(objectivesMap.get(key));
                    if (objective.getIndex() < 0)
                        objective.setIndex(objectiveIndex++);
                }
            }
            // handling conversations/
            int convIndex = 0;
            for (Entry<String, LinkedHashMap<String, String>> entry : data.entrySet()) {
                String key = entry.getKey();
                HashMap<String, String> value = entry.getValue();
                if (key.startsWith("conversations.")) {
                    HashMap<String, String> convData = value;
                    String convName = key.substring(14);
                    Conversation conv = newByID(convName, name -> new Conversation(this, name));
                    if (conv.getIndex() < 0)
                        conv.setIndex(convIndex++);
                    int playerIndex = 0;
                    int npcIndex = 0;
                    // handling conversation.yml
                    for (Entry<String, String> subEntry : convData.entrySet()) {
                        String subKey = subEntry.getKey();
                        String subValue = subEntry.getValue();
                        // reading NPC name, optionally in multiple languages
                        if (subKey.equals("quester")) {
                            conv.getNPC().setDef(subValue);
                        } else if (subKey.startsWith("quester.")) {
                            String lang = subKey.substring(8);
                            if (!languages.containsKey(lang)) {
                                languages.put(lang, 1);
                            } else {
                                languages.put(lang, languages.get(lang) + 1);
                            }
                            conv.getNPC().addLang(lang, subValue);
                        }
                        // reading the stop option
                        else if (subKey.equals("stop")) {
                            conv.getStop().set(subValue.equalsIgnoreCase("true"));
                        }
                        // reading starting options
                        else if (subKey.equals("first")) {
                            String[] pointerNames = subValue.split(",");
                            ArrayList<IdWrapper<NpcOption>> options = new ArrayList<>(pointerNames.length);
                            for (int i = 0; i < pointerNames.length; i++) {
                                IdWrapper<NpcOption> startingOption = new IdWrapper<>(this,
                                        conv.newNpcOption(pointerNames[i].trim()));
                                options.add(i, startingOption);
                                startingOption.setIndex(i);
                            }
                            conv.getStartingOptions().addAll(options);
                        }
                        // reading final events
                        else if (subKey.equals("final")) {
                            String[] eventNames = subValue.split(",");
                            ArrayList<IdWrapper<Event>> events = new ArrayList<>(eventNames.length);
                            for (int i = 0; i < eventNames.length; i++) {
                                IdWrapper<Event> finalEvent = new IdWrapper<>(this,
                                        newByID(eventNames[i].trim(), name -> new Event(this, name)));
                                events.add(i, finalEvent);
                                finalEvent.setIndex(i);
                            }
                            conv.getFinalEvents().addAll(events);
                        }
                        // reading NPC options
                        else if (subKey.startsWith("NPC_options.")) {
                            String[] parts = subKey.split("\\.");
                            if (parts.length > 1) {
                                String optionName = parts[1];
                                // resolving an option
                                NpcOption option = conv.newNpcOption(optionName);
                                if (option.getIndex() < 0)
                                    option.setIndex(npcIndex++);
                                if (parts.length > 2) {
                                    // getting specific values
                                    switch (parts[2]) {
                                    case "text":
                                        if (parts.length > 3) {
                                            String lang = parts[3];
                                            if (!languages.containsKey(lang)) {
                                                languages.put(lang, 1);
                                            } else {
                                                languages.put(lang, languages.get(lang) + 1);
                                            }
                                            option.getText().addLang(lang, subValue);
                                        } else {
                                            option.getText().setDef(subValue);
                                        }
                                        break;
                                    case "event":
                                    case "events":
                                        String[] eventNames = subValue.split(",");
                                        ArrayList<IdWrapper<Event>> events = new ArrayList<>(eventNames.length);
                                        for (int i = 0; i < eventNames.length; i++) {
                                            IdWrapper<Event> event = new IdWrapper<>(this,
                                                    newByID(eventNames[i].trim(), name -> new Event(this, name)));
                                            events.add(i, event);
                                            event.setIndex(i);
                                        }
                                        option.getEvents().addAll(events);
                                        break;
                                    case "condition":
                                    case "conditions":
                                        String[] conditionNames = subValue.split(",");
                                        ArrayList<ConditionWrapper> conditions = new ArrayList<>(
                                                conditionNames.length);
                                        for (int i = 0; i < conditionNames.length; i++) {
                                            String name = conditionNames[i].trim();
                                            boolean negated = false;
                                            while (name.startsWith("!")) {
                                                name = name.substring(1, name.length());
                                                negated = true;
                                            }
                                            ConditionWrapper condition = new ConditionWrapper(this,
                                                    newByID(name, idString -> new Condition(this, idString)));
                                            condition.setNegated(negated);
                                            conditions.add(i, condition);
                                            condition.setIndex(i);
                                        }
                                        option.getConditions().addAll(conditions);
                                        break;
                                    case "pointer":
                                    case "pointers":
                                        String[] pointerNames = subValue.split(",");
                                        ArrayList<IdWrapper<ConversationOption>> options = new ArrayList<>(
                                                pointerNames.length);
                                        for (int i = 0; i < pointerNames.length; i++) {
                                            IdWrapper<ConversationOption> pointer = new IdWrapper<>(this,
                                                    conv.newPlayerOption(pointerNames[i].trim()));
                                            options.add(i, pointer);
                                            pointer.setIndex(i);
                                        }
                                        option.getPointers().addAll(options);
                                        break;
                                    }
                                }
                            }
                        }
                        // reading player options
                        else if (subKey.startsWith("player_options.")) {
                            String[] parts = subKey.split("\\.");
                            if (parts.length > 1) {
                                String optionName = parts[1];
                                // resolving an option
                                PlayerOption option = conv.newPlayerOption(optionName);
                                if (option.getIndex() < 0) {
                                    option.setIndex(playerIndex++);
                                }
                                if (parts.length > 2) {
                                    // getting specific values
                                    switch (parts[2]) {
                                    case "text":
                                        if (parts.length > 3) {
                                            String lang = parts[3];
                                            if (!languages.containsKey(lang)) {
                                                languages.put(lang, 1);
                                            } else {
                                                languages.put(lang, languages.get(lang) + 1);
                                            }
                                            option.getText().addLang(lang, subValue);
                                        } else {
                                            option.getText().setDef(subValue);
                                        }
                                        break;
                                    case "event":
                                    case "events":
                                        String[] eventNames = subValue.split(",");
                                        ArrayList<IdWrapper<Event>> events = new ArrayList<>(eventNames.length);
                                        for (int i = 0; i < eventNames.length; i++) {
                                            IdWrapper<Event> event = new IdWrapper<>(this,
                                                    newByID(eventNames[i].trim(), name -> new Event(this, name)));
                                            events.add(i, event);
                                            event.setIndex(i);
                                        }
                                        option.getEvents().addAll(events);
                                        break;
                                    case "condition":
                                    case "conditions":
                                        String[] conditionNames = subValue.split(",");
                                        ArrayList<ConditionWrapper> conditions = new ArrayList<>(
                                                conditionNames.length);
                                        for (int i = 0; i < conditionNames.length; i++) {
                                            String name = conditionNames[i].trim();
                                            boolean negated = false;
                                            while (name.startsWith("!")) {
                                                name = name.substring(1, name.length());
                                                negated = true;
                                            }
                                            ConditionWrapper condition = new ConditionWrapper(this,
                                                    newByID(name, idString -> new Condition(this, idString)));
                                            condition.setNegated(negated);
                                            conditions.add(i, condition);
                                            condition.setIndex(i);
                                        }
                                        option.getConditions().addAll(conditions);
                                        break;
                                    case "pointer":
                                    case "pointers":
                                        String[] pointerNames = subValue.split(",");
                                        ArrayList<IdWrapper<ConversationOption>> options = new ArrayList<>(
                                                pointerNames.length);
                                        for (int i = 0; i < pointerNames.length; i++) {
                                            IdWrapper<ConversationOption> pointer = new IdWrapper<>(this,
                                                    conv.newNpcOption(pointerNames[i].trim()));
                                            options.add(i, pointer);
                                            pointer.setIndex(i);
                                        }
                                        option.getPointers().addAll(options);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // handling main.yml
            LinkedHashMap<String, String> config = data.get("main");
            for (Entry<String, String> entry : config.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                // handling variables
                if (key.startsWith("variables.")) {
                    variables.add(new GlobalVariable(this, key.substring(10), value));
                }
                // handling global locations
                else if (key.startsWith("global_locations")) {
                    for (String globLoc : value.split(",")) {
                        locations.add(new GlobalLocation(newByID(globLoc, name -> new Objective(this, name))));
                    }
                }
                // handling static events
                else if (key.startsWith("static_events.")) {
                    StaticEvent staticEvent = new StaticEvent(this, key.substring(14));
                    staticEvent.getEvent().set(newByID(value, name -> new Event(this, name)));
                    staticEvents.add(staticEvent);
                }
                // handling NPC-conversation bindings
                else if (key.startsWith("npcs.")) {
                    npcBindings.add(new NpcBinding(this, key.substring(5),
                            newByID(value, name -> new Conversation(this, name))));
                }
                // handling quest cancelers
                else if (key.startsWith("cancel.")) {
                    String[] parts = key.split("\\.");
                    if (parts.length > 1) {
                        // getting the right canceler or creating new one
                        QuestCanceler canceler = newByID(parts[1], name -> new QuestCanceler(this, name));
                        // handling canceler properties
                        if (parts.length > 2) {
                            switch (parts[2]) {
                            case "name":
                                if (parts.length > 3) {
                                    String lang = parts[3];
                                    if (!languages.containsKey(lang)) {
                                        languages.put(lang, 1);
                                    } else {
                                        languages.put(lang, languages.get(lang) + 1);
                                    }
                                    canceler.getName().addLang(lang, value);
                                } else {
                                    canceler.getName().setDef(value);
                                }
                                break;
                            case "events":
                                String[] eventNames = value.split(",");
                                ArrayList<IdWrapper<Event>> events = new ArrayList<>(eventNames.length);
                                for (int i = 0; i < eventNames.length; i++) {
                                    IdWrapper<Event> event = new IdWrapper<>(this,
                                            newByID(eventNames[i].trim(), name -> new Event(this, name)));
                                    events.add(i, event);
                                    event.setIndex(i);
                                }
                                canceler.getEvents().addAll(events);
                                break;
                            case "conditions":
                                String[] conditionNames = value.split(",");
                                ArrayList<ConditionWrapper> conditions = new ArrayList<>(conditionNames.length);
                                for (int i = 0; i < conditionNames.length; i++) {
                                    String name = conditionNames[i].trim();
                                    boolean negated = false;
                                    while (name.startsWith("!")) {
                                        name = name.substring(1, name.length());
                                        negated = true;
                                    }
                                    ConditionWrapper condition = new ConditionWrapper(this,
                                            newByID(name, idString -> new Condition(this, idString)));
                                    condition.setNegated(negated);
                                    conditions.add(i, condition);
                                    condition.setIndex(i);
                                }
                                canceler.getConditions().addAll(conditions);
                                break;
                            case "objectives":
                                String[] objectiveNames = value.split(",");
                                ArrayList<IdWrapper<Objective>> objectives = new ArrayList<>(objectiveNames.length);
                                for (int i = 0; i < objectiveNames.length; i++) {
                                    IdWrapper<Objective> wrapper = new IdWrapper<>(this,
                                            newByID(objectiveNames[i].trim(), name -> new Objective(this, name)));
                                    objectives.add(i, wrapper);
                                    wrapper.setIndex(i);
                                }
                                canceler.getObjectives().addAll(objectives);
                                break;
                            case "tags":
                                String[] tagNames = value.split(",");
                                ArrayList<IdWrapper<Tag>> tags = new ArrayList<>(tagNames.length);
                                for (int i = 0; i < tagNames.length; i++) {
                                    IdWrapper<Tag> wrapper = new IdWrapper<>(this,
                                            newByID(tagNames[i].trim(), name -> new Tag(this, name)));
                                    tags.add(i, wrapper);
                                    wrapper.setIndex(i);
                                }
                                canceler.getTags().addAll(tags);
                                break;
                            case "points":
                                String[] pointNames = value.split(",");
                                ArrayList<IdWrapper<PointCategory>> points = new ArrayList<>(pointNames.length);
                                for (int i = 0; i < pointNames.length; i++) {
                                    IdWrapper<PointCategory> wrapper = new IdWrapper<>(this,
                                            newByID(pointNames[i].trim(), name -> new PointCategory(this, name)));
                                    points.add(i, wrapper);
                                    wrapper.setIndex(i);
                                }
                                canceler.getPoints().addAll(points);
                                break;
                            case "journal":
                                String[] journalNames = value.split(",");
                                ArrayList<IdWrapper<JournalEntry>> journal = new ArrayList<>(journalNames.length);
                                for (int i = 0; i < journalNames.length; i++) {
                                    IdWrapper<JournalEntry> wrapper = new IdWrapper<>(this,
                                            newByID(journalNames[i].trim(), name -> new JournalEntry(this, name)));
                                    journal.add(i, wrapper);
                                    wrapper.setIndex(i);
                                }
                                canceler.getJournal().addAll(journal);
                                break;
                            case "loc":
                                canceler.setLocation(value);
                                break;
                            }
                        }
                    }
                }
                // handling journal main page
                else if (key.startsWith("journal_main_page")) {
                    String[] parts = key.split("\\.");
                    if (parts.length > 1) {
                        MainPageLine line = newByID(parts[1], name -> new MainPageLine(this, name));
                        if (parts.length > 2) {
                            switch (parts[2]) {
                            case "text":
                                if (parts.length > 3) {
                                    String lang = parts[3];
                                    if (!languages.containsKey(lang)) {
                                        languages.put(lang, 1);
                                    } else {
                                        languages.put(lang, languages.get(lang) + 1);
                                    }
                                    line.getText().addLang(lang, value);
                                } else {
                                    line.getText().setDef(value);
                                }
                                break;
                            case "priority":
                                try {
                                    line.getPriority().set(Integer.parseInt(value));
                                } catch (NumberFormatException e) {
                                    // TODO error, need a number
                                }
                                break;
                            case "conditions":
                                String[] conditionNames = value.split(",");
                                ArrayList<ConditionWrapper> conditions = new ArrayList<>(conditionNames.length);
                                for (int i = 0; i < conditionNames.length; i++) {
                                    String name = conditionNames[i].trim();
                                    boolean negated = false;
                                    while (name.startsWith("!")) {
                                        name = name.substring(1, name.length());
                                        negated = true;
                                    }
                                    ConditionWrapper condition = new ConditionWrapper(this,
                                            newByID(name, idString -> new Condition(this, idString)));
                                    condition.setNegated(negated);
                                    conditions.add(i, condition);
                                    condition.setIndex(i);
                                }
                                line.getConditions().addAll(conditions);
                                break;
                            }
                        }
                    }
                }
                // handling default language
                else if (key.equalsIgnoreCase("default_language")) {
                    defLang = value;
                }
            }
            // check which language is used most widely and set it as default
            if (defLang == null) {
                int max = 0;
                String maxLang = null;
                for (Entry<String, Integer> entry : languages.entrySet()) {
                    if (entry.getValue() > max) {
                        max = entry.getValue();
                        maxLang = entry.getKey();
                    }
                }
                defLang = maxLang;
            }
        } catch (Exception e) {
            ExceptionController.display(e);
        }
    }

    @Override
    public boolean edit() {
        return NameEditController.display(packName);
    }

    public PackageSet getSet() {
        return set;
    }

    public StringProperty getName() {
        return packName;
    }

    public String getDefLang() {
        return defLang;
    }

    public void setDefLang(String defLang) {
        this.defLang = defLang;
    }

    public ObservableList<Conversation> getConversations() {
        return conversations;
    }

    public ObservableList<Event> getEvents() {
        return events;
    }

    public ObservableList<Condition> getConditions() {
        return conditions;
    }

    public ObservableList<Objective> getObjectives() {
        return objectives;
    }

    public ObservableList<JournalEntry> getJournal() {
        return journal;
    }

    public ObservableList<Item> getItems() {
        return items;
    }

    public ObservableList<GlobalVariable> getVariables() {
        return variables;
    }

    public ObservableList<GlobalLocation> getLocations() {
        return locations;
    }

    public ObservableList<StaticEvent> getStaticEvents() {
        return staticEvents;
    }

    public ObservableList<QuestCanceler> getCancelers() {
        return cancelers;
    }

    public ObservableList<NpcBinding> getNpcBindings() {
        return npcBindings;
    }

    public ObservableList<MainPageLine> getMainPage() {
        return mainPage;
    }

    public ObservableList<Tag> getTags() {
        return tags;
    }

    public ObservableList<PointCategory> getPoints() {
        return points;
    }

    /**
     * @return all NpcOptions from loaded conversations in this package, the ones from current conversation first
     */
    public ObservableList<NpcOption> getAllNpcOptions() {
        ObservableList<NpcOption> list = FXCollections.observableArrayList();
        list.addAll(ConversationController.getDisplayedConversation().getNpcOptions());
        conversations.forEach(conv -> {
            if (!conv.equals(ConversationController.getDisplayedConversation())) {
                list.addAll(conv.getNpcOptions());
            }
        });
        return list;
    }

    /**
     * @return all PlayerOptions from loaded conversations in this package, the ones from current conversation first
     */
    public ObservableList<PlayerOption> getAllPlayerOptions() {
        ObservableList<PlayerOption> list = FXCollections.observableArrayList();
        list.addAll(ConversationController.getDisplayedConversation().getPlayerOptions());
        conversations.forEach(conv -> {
            if (!conv.equals(ConversationController.getDisplayedConversation())) {
                list.addAll(conv.getPlayerOptions());
            }
        });
        return list;
    }

    public void sort() {
        // sort lists
        ArrayList<ObservableList<? extends ID>> lists = new ArrayList<>();
        lists.add(conversations);
        lists.add(events);
        lists.add(conditions);
        lists.add(objectives);
        lists.add(items);
        lists.add(journal);
        lists.add(staticEvents);
        lists.add(variables);
        lists.add(npcBindings);
        lists.add(mainPage);
        for (Conversation conv : conversations) {
            lists.add(conv.getNpcOptions());
            lists.add(conv.getPlayerOptions());
            lists.add(conv.getStartingOptions());
            lists.add(conv.getFinalEvents());
            ArrayList<ConversationOption> list = new ArrayList<>(conv.getNpcOptions());
            list.addAll(conv.getPlayerOptions());
            for (ConversationOption option : list) {
                lists.add(option.getConditions());
                lists.add(option.getEvents());
                lists.add(option.getPointers());
            }
        }
        for (ObservableList<? extends ID> list : lists) {
            list.sort((ID o1, ID o2) -> o1.getIndex() - o2.getIndex());
            int index = 0;
            for (ID object : list) {
                object.setIndex(index++);
            }
        }
    }

    @Override
    public String toString() {
        return packName.get();
    }

    public <T extends ID> T newByID(String id, Generator<T> generator) {
        T object = generator.generate(id);
        ObservableList<T> list = object.getList();
        T existing = null;
        for (T check : list) {
            if (check.getId().get().equals(id)) {
                existing = check;
                break;
            }
        }
        if (existing == null) {
            existing = object;
            list.add(existing);
        }
        return existing;
    }

    public interface Generator<T> {
        public T generate(String id);
    }

    public void printMainYAML(OutputStream out) throws IOException {
        YAMLFactory yf = new YAMLFactory();
        YAMLMapper mapper = new YAMLMapper();
        ObjectNode root = mapper.createObjectNode();
        // save NPCs
        if (!npcBindings.isEmpty()) {
            ObjectNode npcs = mapper.createObjectNode();
            for (NpcBinding binding : npcBindings) {
                npcs.put(binding.getId().get(), binding.getConversation().get().getId().get());
            }
            root.set("npcs", npcs);
        }
        // save global variables
        if (!variables.isEmpty()) {
            ObjectNode variables = mapper.createObjectNode();
            for (GlobalVariable var : this.variables) {
                variables.put(var.getId().get(), var.getInstruction().get());
            }
            root.set("variables", variables);
        }
        // save static events
        if (!staticEvents.isEmpty()) {
            ObjectNode staticEvents = mapper.createObjectNode();
            for (StaticEvent event : this.staticEvents) {
                staticEvents.put(event.getId().get(), event.getEvent().get().getId().get());
            }
            root.set("static", staticEvents);
        }
        // save global locations
        if (!locations.isEmpty()) {
            StringBuilder builder = new StringBuilder();
            for (GlobalLocation loc : locations) {
                builder.append(loc.toString() + ",");
            }
            root.put("global_locations", builder.toString().substring(0, builder.length() - 1));
        }
        // save quest cancelers
        if (!cancelers.isEmpty()) {
            ObjectNode cancelers = mapper.createObjectNode();
            for (QuestCanceler canceler : this.cancelers) {
                ObjectNode cancelerNode = mapper.createObjectNode();
                addTranslatedNode(mapper, cancelerNode, "name", canceler.getName());
                if (!canceler.getEvents().isEmpty()) {
                    StringBuilder events = new StringBuilder();
                    for (IdWrapper<Event> event : canceler.getEvents()) {
                        events.append(event.toString() + ',');
                    }
                    cancelerNode.put("events", events.toString().substring(0, events.length() - 1));
                }
                if (!canceler.getConditions().isEmpty()) {
                    StringBuilder conditions = new StringBuilder();
                    for (ConditionWrapper condition : canceler.getConditions()) {
                        conditions.append(condition.toString() + ',');
                    }
                    cancelerNode.put("conditions", conditions.toString().substring(0, conditions.length() - 1));
                }
                if (!canceler.getObjectives().isEmpty()) {
                    StringBuilder objectives = new StringBuilder();
                    for (IdWrapper<Objective> objective : canceler.getObjectives()) {
                        objectives.append(objective.toString() + ',');
                    }
                    cancelerNode.put("objectives", objectives.toString().substring(0, objectives.length() - 1));
                }
                if (!canceler.getTags().isEmpty()) {
                    StringBuilder tags = new StringBuilder();
                    for (IdWrapper<Tag> tag : canceler.getTags()) {
                        tags.append(tag.toString() + ',');
                    }
                    cancelerNode.put("tags", tags.toString().substring(0, tags.length() - 1));
                }
                if (!canceler.getPoints().isEmpty()) {
                    StringBuilder points = new StringBuilder();
                    for (IdWrapper<PointCategory> point : canceler.getPoints()) {
                        points.append(point.toString() + ',');
                    }
                    cancelerNode.put("points", points.toString().substring(0, points.length() - 1));
                }
                if (!canceler.getJournal().isEmpty()) {
                    StringBuilder journals = new StringBuilder();
                    for (IdWrapper<JournalEntry> journal : canceler.getJournal()) {
                        journals.append(journal.toString() + ',');
                    }
                    cancelerNode.put("journal", journals.toString().substring(0, journals.length() - 1));
                }
                if (canceler.getLocation() != null) {
                    cancelerNode.put("loc", canceler.getLocation());
                }
                cancelers.set(canceler.getId().get(), cancelerNode);
            }
            root.set("cancel", cancelers);
        }
        // save main page
        if (!mainPage.isEmpty()) {
            ObjectNode lines = mapper.createObjectNode();
            for (MainPageLine line : mainPage) {
                ObjectNode node = mapper.createObjectNode();
                addTranslatedNode(mapper, node, "text", line.getText());
                node.put("priority", line.getPriority().get());
                StringBuilder conditions = new StringBuilder();
                for (ConditionWrapper condition : line.getConditions()) {
                    conditions.append(condition.toString() + ',');
                }
                node.put("conditions", conditions.substring(0, conditions.length() - 1));
                lines.set(line.getId().get(), node);
            }
            root.set("journal_main_page", lines);
        }
        yf.createGenerator(out).setCodec(mapper).writeObject(root);
    }

    public void printEventsYaml(OutputStream out) throws IOException {
        YAMLFactory yf = new YAMLFactory();
        YAMLMapper mapper = new YAMLMapper();
        ObjectNode root = mapper.createObjectNode();
        for (Event event : events) {
            root.put(event.getId().get(), event.getInstruction().get());
        }
        yf.createGenerator(out).setCodec(mapper).writeObject(root);
    }

    public void printConditionsYaml(OutputStream out) throws IOException {
        YAMLFactory yf = new YAMLFactory();
        YAMLMapper mapper = new YAMLMapper();
        ObjectNode root = mapper.createObjectNode();
        for (Condition condition : conditions) {
            root.put(condition.getId().get(), condition.getInstruction().get());
        }
        yf.createGenerator(out).setCodec(mapper).writeObject(root);
    }

    public void printObjectivesYaml(OutputStream out) throws IOException {
        YAMLFactory yf = new YAMLFactory();
        YAMLMapper mapper = new YAMLMapper();
        ObjectNode root = mapper.createObjectNode();
        for (Objective objective : objectives) {
            root.put(objective.getId().get(), objective.getInstruction().get());
        }
        yf.createGenerator(out).setCodec(mapper).writeObject(root);
    }

    public void printItemsYaml(OutputStream out) throws IOException {
        YAMLFactory yf = new YAMLFactory();
        YAMLMapper mapper = new YAMLMapper();
        ObjectNode root = mapper.createObjectNode();
        for (Item item : items) {
            root.put(item.getId().get(), item.getInstruction().get());
        }
        yf.createGenerator(out).setCodec(mapper).writeObject(root);
    }

    public void printJournalYaml(OutputStream out) throws IOException {
        YAMLFactory yf = new YAMLFactory();
        YAMLMapper mapper = new YAMLMapper();
        ObjectNode root = mapper.createObjectNode();
        for (JournalEntry entry : journal) {
            addTranslatedNode(mapper, root, entry.getId().get(), entry.getText());
        }
        yf.createGenerator(out).setCodec(mapper).writeObject(root);
    }

    public void printConversationYaml(OutputStream out, Conversation conv) throws IOException {
        YAMLFactory yf = new YAMLFactory();
        YAMLMapper mapper = new YAMLMapper();
        ObjectNode root = mapper.createObjectNode();
        addTranslatedNode(mapper, root, "quester", conv.getNPC());
        root.put("stop", String.valueOf(conv.getStop().get()));
        StringBuilder first = new StringBuilder();
        for (IdWrapper<NpcOption> option : conv.getStartingOptions()) {
            first.append(option.toString() + ',');
        }
        root.put("first", first.substring(0, first.length() - 1));
        if (!conv.getFinalEvents().isEmpty()) {
            StringBuilder finalEvents = new StringBuilder();
            for (IdWrapper<Event> event : conv.getFinalEvents()) {
                finalEvents.append(event.toString() + ',');
            }
            root.put("final", finalEvents.substring(0, finalEvents.length() - 1));
        }
        if (!conv.getNpcOptions().isEmpty()) {
            ObjectNode npcOptions = mapper.createObjectNode();
            for (NpcOption option : conv.getNpcOptions()) {
                ObjectNode npcOption = mapper.createObjectNode();
                addTranslatedNode(mapper, npcOption, "text", option.getText());
                if (!option.getEvents().isEmpty()) {
                    StringBuilder events = new StringBuilder();
                    for (IdWrapper<Event> event : option.getEvents()) {
                        events.append(event.toString() + ',');
                    }
                    npcOption.put("events", events.substring(0, events.length() - 1));
                }
                if (!option.getConditions().isEmpty()) {
                    StringBuilder conditions = new StringBuilder();
                    for (IdWrapper<Condition> condition : option.getConditions()) {
                        conditions.append(condition.toString() + ',');
                    }
                    npcOption.put("conditions", conditions.substring(0, conditions.length() - 1));
                }
                if (!option.getPointers().isEmpty()) {
                    StringBuilder pointers = new StringBuilder();
                    for (IdWrapper<ConversationOption> pointer : option.getPointers()) {
                        pointers.append(pointer.toString() + ',');
                    }
                    npcOption.put("pointers", pointers.substring(0, pointers.length() - 1));
                }
                npcOptions.set(option.getId().get(), npcOption);
            }
            root.set("NPC_options", npcOptions);
        }
        if (!conv.getPlayerOptions().isEmpty()) {
            ObjectNode playerOptions = mapper.createObjectNode();
            for (PlayerOption option : conv.getPlayerOptions()) {
                ObjectNode playerOption = mapper.createObjectNode();
                addTranslatedNode(mapper, playerOption, "text", option.getText());
                if (!option.getEvents().isEmpty()) {
                    StringBuilder events = new StringBuilder();
                    for (IdWrapper<Event> event : option.getEvents()) {
                        events.append(event.toString() + ',');
                    }
                    playerOption.put("events", events.substring(0, events.length() - 1));
                }
                if (!option.getConditions().isEmpty()) {
                    StringBuilder conditions = new StringBuilder();
                    for (IdWrapper<Condition> condition : option.getConditions()) {
                        conditions.append(condition.toString() + ',');
                    }
                    playerOption.put("conditions", conditions.substring(0, conditions.length() - 1));
                }
                if (!option.getPointers().isEmpty()) {
                    StringBuilder pointers = new StringBuilder();
                    for (IdWrapper<ConversationOption> pointer : option.getPointers()) {
                        pointers.append(pointer.toString() + ',');
                    }
                    playerOption.put("pointers", pointers.substring(0, pointers.length() - 1));
                }
                playerOptions.set(option.getId().get(), playerOption);
            }
            root.set("player_options", playerOptions);
        }
        yf.createGenerator(out).setCodec(mapper).writeObject(root);
    }

    private void addTranslatedNode(YAMLMapper mapper, ObjectNode root, String name, TranslatableText text) {
        if (text.getDef() != null) {
            root.put(name, text.getDef().get());
        } else {
            ObjectNode node = mapper.createObjectNode();
            for (String lang : text.getLanguages()) {
                if (lang == null) { // TODO find out why there's a null language
                    continue;
                }
                node.put(lang, text.get(lang).get());
            }
            root.set(name, node);
        }
    }

}