/**
* 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.database;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.bukkit.inventory.ItemStack;
import pl.betoncraft.betonquest.BetonQuest;
import pl.betoncraft.betonquest.InstructionParseException;
import pl.betoncraft.betonquest.Journal;
import pl.betoncraft.betonquest.ObjectNotFoundException;
import pl.betoncraft.betonquest.ObjectiveID;
import pl.betoncraft.betonquest.Point;
import pl.betoncraft.betonquest.Pointer;
import pl.betoncraft.betonquest.api.Objective;
import pl.betoncraft.betonquest.config.Config;
import pl.betoncraft.betonquest.config.QuestCanceler;
import pl.betoncraft.betonquest.database.Connector.QueryType;
import pl.betoncraft.betonquest.database.Connector.UpdateType;
import pl.betoncraft.betonquest.database.Saver.Record;
import pl.betoncraft.betonquest.item.QuestItem;
import pl.betoncraft.betonquest.utils.Debug;
import pl.betoncraft.betonquest.utils.PlayerConverter;
/**
* Represents an object storing all player-related data, which can load and save it.
*
* @author Jakub Sapalski
*/
public class PlayerData {
private Saver saver = BetonQuest.getInstance().getSaver();
private String playerID;
private List<String> tags = new ArrayList<>();
private List<Pointer> entries = new ArrayList<>();
private List<Point> points = new ArrayList<>();
private HashMap<String, String> objectives = new HashMap<>(); // not active ones
private Journal journal;
private List<ItemStack> backpack = new ArrayList<>();
private String conv;
private String lang; // the player's language
/**
* Creates new PlayerData for the player represented by playerID.
*
* @param playerID
* - ID of the player
*/
public PlayerData(String playerID) {
this.playerID = playerID;
// load data from the database
loadAllPlayerData();
}
/**
* Loads all data for the player and puts it in appropriate lists.
*/
public void loadAllPlayerData() {
try {
// get connection to the database
Connector con = new Connector();
// load objectives
ResultSet res1 = con.querySQL(QueryType.SELECT_OBJECTIVES, new String[] { playerID });
// put them into the list
while (res1.next()) {
objectives.put(res1.getString("objective"), res1.getString("instructions"));
}
// load tags
ResultSet res2 = con.querySQL(QueryType.SELECT_TAGS, new String[] { playerID });
// put them into the list
while (res2.next())
tags.add(res2.getString("tag"));
// load journals
ResultSet res3 = con.querySQL(QueryType.SELECT_JOURNAL, new String[] { playerID });
// put them into the list
while (res3.next()) {
entries.add(new Pointer(res3.getString("pointer"), res3.getTimestamp("date").getTime()));
}
// load points
ResultSet res4 = con.querySQL(QueryType.SELECT_POINTS, new String[] { playerID });
// put them into the list
while (res4.next())
points.add(new Point(res4.getString("category"), res4.getInt("count")));
// load backpack
ResultSet res5 = con.querySQL(QueryType.SELECT_BACKPACK, new String[] { playerID });
// put items into the list
while (res5.next()) {
String instruction = res5.getString("instruction");
int amount = res5.getInt("amount");
ItemStack item;
try {
item = new QuestItem(instruction).generate(amount);
} catch (InstructionParseException e) {
Debug.error("Could not load backpack item for player " + PlayerConverter.getName(playerID)
+ ", with instruction '" + instruction + "', because: " + e.getMessage());
continue;
}
backpack.add(item);
}
// load language
ResultSet res6 = con.querySQL(QueryType.SELECT_PLAYER, new String[] { playerID });
// put it there
if (res6.next()) {
lang = res6.getString("language");
if (lang.equals("default")) {
lang = Config.getLanguage();
}
conv = res6.getString("conversation");
if (conv == null || conv.equalsIgnoreCase("null")) {
conv = null;
}
} else {
lang = Config.getLanguage();
saver.add(new Record(UpdateType.ADD_PLAYER, new String[] { playerID, "default" }));
}
// log data to debugger
if (Debug.debugging()) {
Debug.info("There are " + objectives.size() + " objectives, " + tags.size() + " tags, " + points.size()
+ " points, " + entries.size() + " journal entries and " + backpack.size()
+ " items loaded for player " + PlayerConverter.getName(playerID));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* Returns the List of Tags for this player.
*
* @return the List of Tags
*/
public List<String> getTags() {
return tags;
}
/**
* Checks if the player has specified tag.
*
* @param tag
* tag to check
* @return true if the player has this tag
*/
public boolean hasTag(String tag) {
return tags.contains(tag);
}
/**
* Adds the specified tag to player's list. It won't double it however.
*
* @param tag
* tag to add
*/
public void addTag(String tag) {
if (!tags.contains(tag)) {
tags.add(tag);
saver.add(new Record(UpdateType.ADD_TAGS, new String[] { playerID, tag }));
}
}
/**
* Removes the specified tag from player's list. If there is no tag, nothing
* happens.
*
* @param tag
* tag to remove
*/
public void removeTag(String tag) {
tags.remove(tag);
saver.add(new Record(UpdateType.REMOVE_TAGS, new String[] { playerID, tag }));
}
/**
* Returns the List of Points for this player.
*
* @return the List of Points
*/
public List<Point> getPoints() {
return points;
}
/**
* Returns the amount of point the player has in specified category. If the
* category does not exist, it will return 0.
*
* @param category
* name of the category
* @return amount of points
*/
public int hasPointsFromCategory(String category) {
for (Point p : points) {
if (p.getCategory().equals(category)) {
return p.getCount();
}
}
return 0;
}
/**
* Adds or subtracts points to/from specified category. If there is no such category it will
* be created.
*
* @param category
* points will be added to this category
* @param count
* how much points will be added (or subtracted if negative)
*/
public void modifyPoints(String category, int count) {
saver.add(new Record(UpdateType.REMOVE_POINTS, new String[] { playerID, category }));
// check if the category already exists
for (Point point : points) {
if (point.getCategory().equalsIgnoreCase(category)) {
// if it does, add points to it
saver.add(new Record(UpdateType.ADD_POINTS,
new String[] { playerID, category, String.valueOf(point.getCount() + count) }));
point.addPoints(count);
return;
}
}
// if not then create new point category with given amount of points
points.add(new Point(category, count));
saver.add(new Record(UpdateType.ADD_POINTS, new String[] { playerID, category, String.valueOf(count) }));
}
/**
* Removes the whole category of points.
*
* @param category
* name of a point category
*/
public void removePointsCategory(String category) {
Point pointToRemove = null;
for (Point point : points) {
if (point.getCategory().equalsIgnoreCase(category)) {
pointToRemove = point;
}
}
if (pointToRemove != null) {
points.remove(pointToRemove);
}
saver.add(new Record(UpdateType.REMOVE_POINTS, new String[] { playerID, category }));
}
/**
* Returns a Journal instance or creates it if it does not exist.
*
* @return new Journal instance
*/
public Journal getJournal() {
if (journal == null) {
journal = new Journal(playerID, lang, entries);
}
return journal;
}
/**
* Starts all Objectives for this player. It takes all "raw" objectives and
* initializes them. Raw objectives are deleted from their HashMap after
* this action (so they won't be started twice)
*/
public void startObjectives() {
for (String objective : objectives.keySet()) {
try {
ObjectiveID objectiveID = new ObjectiveID(null, objective);
BetonQuest.resumeObjective(playerID, objectiveID, objectives.get(objective));
} catch (ObjectNotFoundException e) {
Debug.error("Loaded '" + objective
+ "' objective from the database, but it is not defined in configuration. Skipping.");
}
}
objectives.clear();
}
/**
* @return the map containing objective IDs and their objective data; these
* are not initialized yet
*/
public HashMap<String, String> getRawObjectives() {
return objectives;
}
/**
* Adds new objective to a list of not initialized objectives. It's added to the
* database and can be started by running {@link #startObjectives()}.
*
* @param objectiveID
* ID of the objective
*/
public void addNewRawObjective(ObjectiveID objectiveID) {
Objective obj = BetonQuest.getInstance().getObjective(objectiveID);
if (obj == null) {
return;
}
String data = obj.getDefaultDataInstruction();
if (addRawObjective(objectiveID.toString(), data))
saver.add(new Record(UpdateType.ADD_OBJECTIVES, new String[]{playerID, objectiveID.toString(), data}));
}
/**
* Adds objective to a list of not initialized objectives. This does not add
* the objective to the database because it's not a new objective, hence
* it's already in the database.
*
* @param objectiveID
* ID of the objective
* @param data
* data instruction string to use
* @return true if the objective was successfully added, false if it was
* already there
*/
public boolean addRawObjective(String objectiveID, String data) {
if (objectives.containsKey(objectiveID)) {
return false;
}
objectives.put(objectiveID, data);
return true;
}
/**
* Removes not initialized objective from the plugin and the database.
*
* @param objectiveID
* the ID of the objective
*/
public void removeRawObjective(ObjectiveID objectiveID) {
objectives.remove(objectiveID.toString());
removeObjFromDB(objectiveID.toString());
}
/**
* Directly adds specified objectiveID and data string to the database.
*
* @param objectiveID
* the ID of the objective
* @param data
* the data string of this objective (the one associated with ObjectiveData)
*/
public void addObjToDB(String objectiveID, String data) {
saver.add(new Record(UpdateType.ADD_OBJECTIVES, new String[] { playerID, objectiveID, data }));
}
/**
* Directly removes from the database specified objective.
*
* @param objectiveID
* the ID of the objective to remove
*/
public void removeObjFromDB(String objectiveID) {
saver.add(new Record(UpdateType.REMOVE_OBJECTIVES, new String[] { playerID, objectiveID }));
}
/**
* Returns player's backpack as the list of itemstacks.
*
* @return list of itemstacks
*/
public List<ItemStack> getBackpack() {
return backpack;
}
/**
* Updates the database with a list of backpack items.
*
* @param list
* list of all items in the backpack
*/
public void setBackpack(List<ItemStack> list) {
this.backpack = list;
// update the database (quite expensive way, should be changed)
saver.add(new Record(UpdateType.DELETE_BACKPACK, new String[] { playerID }));
for (ItemStack itemStack : list) {
String instruction = QuestItem.itemToString(itemStack);
String amount = String.valueOf(itemStack.getAmount());
saver.add(new Record(UpdateType.ADD_BACKPACK, new String[] { playerID, instruction, amount }));
}
}
/**
* Adds the item to backpack. The amount of the itemstack doesn't matter,
* it's overwritten by amount parameter. Amount can be greater than max
* stack size.
*
* @param item
* ItemStack to add to backpack
* @param amount
* amount of the items
*/
public void addItem(ItemStack item, int amount) {
for (ItemStack itemStack : backpack) {
if (item.isSimilar(itemStack)) {
// if items are similar they can be joined in a single itemstack
if (amount + itemStack.getAmount() <= itemStack.getMaxStackSize()) {
// if they will fit all together, then just add them
itemStack.setAmount(itemStack.getAmount() + amount);
amount = 0; // this will allow for passing the while loop
break;
} else {
// if the stack will be overflown, set max size and continue
amount -= itemStack.getMaxStackSize() - itemStack.getAmount();
itemStack.setAmount(itemStack.getMaxStackSize());
}
}
}
// every item checked, time to add a new itemstack
while (amount > 0) {
// if the amount is greater than max size of the itemstack, create
// max
// stacks until it's lower
ItemStack newItem = item.clone();
int maxSize = newItem.getType().getMaxStackSize();
if (amount > maxSize) {
if (maxSize == 0) {
maxSize = 64;
}
newItem.setAmount(maxSize);
amount -= maxSize;
} else {
newItem.setAmount(amount);
amount = 0;
}
backpack.add(newItem);
}
// update the database (quite expensive way, should be changed)
saver.add(new Record(UpdateType.DELETE_BACKPACK, new String[] { playerID }));
for (ItemStack itemStack : backpack) {
String instruction = QuestItem.itemToString(itemStack);
String newAmount = String.valueOf(itemStack.getAmount());
saver.add(new Record(UpdateType.ADD_BACKPACK, new String[] { playerID, instruction, newAmount }));
}
}
/**
* Cancels the quest by removing all defined tags, points, objectives etc.
*
* @param name
* name of the canceler
*/
public void cancelQuest(String name) {
QuestCanceler canceler = Config.getCancelers().get(name);
if (canceler != null)
canceler.cancel(playerID);
}
/**
* @return the language this player uses
*/
public String getLanguage() {
return lang;
}
/**
* Sets player's language
*
* @param lang
* language to set
*/
public void setLanguage(String lang) {
if (lang.equalsIgnoreCase("default")) {
this.lang = Config.getLanguage();
} else {
this.lang = lang;
}
saver.add(new Record(UpdateType.DELETE_PLAYER, new String[] { playerID }));
saver.add(new Record(UpdateType.ADD_PLAYER, new String[] { playerID, lang }));
}
/**
* @return the name of a conversation if the player has active one or
* null if he does not.
*/
public String getConversation() {
return conv;
}
/**
* Purges all player's data from the database and from this object.
*/
public void purgePlayer() {
for (Objective obj : BetonQuest.getInstance().getPlayerObjectives(playerID)) {
obj.removePlayer(playerID);
}
// clear all lists
objectives.clear();
tags.clear();
points.clear();
entries.clear();
getJournal().clear(); // journal can be null, so use a method to get it
backpack.clear();
// clear the database
saver.add(new Record(UpdateType.DELETE_OBJECTIVES, new String[] { playerID }));
saver.add(new Record(UpdateType.DELETE_JOURNAL, new String[] { playerID }));
saver.add(new Record(UpdateType.DELETE_POINTS, new String[] { playerID }));
saver.add(new Record(UpdateType.DELETE_TAGS, new String[] { playerID }));
saver.add(new Record(UpdateType.DELETE_BACKPACK, new String[] { playerID }));
saver.add(new Record(UpdateType.UPDATE_CONVERSATION, new String[] { "null", playerID }));
// update the journal so it's empty
if (PlayerConverter.getPlayer(playerID) != null) {
getJournal().update();
}
}
}