com.jabyftw.lobstercraft.world.CityStructure.java Source code

Java tutorial

Introduction

Here is the source code for com.jabyftw.lobstercraft.world.CityStructure.java

Source

package com.jabyftw.lobstercraft.world;

import com.jabyftw.lobstercraft.ConfigurationValues;
import com.jabyftw.lobstercraft.LobsterCraft;
import com.jabyftw.lobstercraft.player.OfflinePlayer;
import com.jabyftw.lobstercraft.player.OnlinePlayer;
import com.jabyftw.lobstercraft.services.services_event.PlayerChangesCityOccupationEvent;
import com.jabyftw.lobstercraft.services.services_event.PlayerJoinsCityEvent;
import com.jabyftw.lobstercraft.util.DatabaseState;
import com.jabyftw.lobstercraft.util.InventoryHolder;
import com.jabyftw.lobstercraft.util.Util;
import com.sun.istack.internal.NotNull;
import com.sun.istack.internal.Nullable;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.NumberConversions;

import java.sql.*;
import java.util.*;

/**
 * Copyright (C) 2016  Rafael Sartori for LobsterCraft Plugin
 * <p/>
 * 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.
 * <p/>
 * 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.
 * <p/>
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * <p/>
 * Email address: rafael.sartori96@gmail.com
 */
public class CityStructure {

    /*
     * Leveling constants
     */
    private final static byte MAXIMUM_CITY_LEVEL = 10;
    private final static int INITIAL_NUMBER_OF_ITEMS_INVENTORY = LobsterCraft.configuration
            .getInt(ConfigurationValues.CITY_LEVELING_INITIAL_INVENTORY_ITEMS.toString()),
            MAXIMUM_NUMBER_OF_ITEMS_INVENTORY = LobsterCraft.configuration
                    .getInt(ConfigurationValues.CITY_LEVELING_MAXIMUM_AMOUNT_OF_ITEM_INVENTORY.toString()),
            INITIAL_NUMBER_OF_ITEMS_STORE = LobsterCraft.configuration
                    .getInt(ConfigurationValues.CITY_LEVELING_INITIAL_STORE_ITEMS.toString()),
            MAXIMUM_NUMBER_OF_ITEMS_STORE = LobsterCraft.configuration
                    .getInt(ConfigurationValues.CITY_LEVELING_MAXIMUM_AMOUNT_OF_ITEM_STORE.toString()),
            INITIAL_NUMBER_OF_CITIZENS = LobsterCraft.configuration
                    .getInt(ConfigurationValues.CITY_LEVELING_INITIAL_PLAYER_AMOUNT.toString());
    private final static double NUMBER_OF_ITEMS_STORE_PER_LEVEL = LobsterCraft.configuration
            .getDouble(ConfigurationValues.CITY_LEVELING_STORE_ITEMS_PER_LEVEL.toString()),
            NUMBER_OF_ITEMS_INVENTORY_PER_LEVEL = LobsterCraft.configuration
                    .getDouble(ConfigurationValues.CITY_LEVELING_INVENTORY_ITEMS_PER_LEVEL.toString()),
            NUMBER_OF_CITIZENS_PER_LEVEL = LobsterCraft.configuration
                    .getDouble(ConfigurationValues.CITY_LEVELING_PLAYER_PER_LEVEL.toString()),
            INITIAL_UPGRADE_COST = LobsterCraft.configuration
                    .getDouble(ConfigurationValues.CITY_LEVELING_INITIAL_COST.toString()),
            FACTOR_OF_INCREASE_OF_COST = LobsterCraft.configuration
                    .getDouble(ConfigurationValues.CITY_LEVELING_COST_MULTIPLIER.toString());

    /*
     * Configuration constants
     */
    protected final static double BUILDER_MAXIMUM_RATIO = LobsterCraft.configuration
            .getDouble(ConfigurationValues.CITY_BUILDER_RATIO.toString()),
            MAXIMUM_TAX = LobsterCraft.configuration.getDouble(ConfigurationValues.CITY_MAXIMUM_TAX.toString()),
            MINIMUM_TAX = LobsterCraft.configuration.getDouble(ConfigurationValues.CITY_MINIMUM_TAX.toString());

    /*
     * Database information
     */
    private final int cityId;
    private final String cityName;
    protected byte cityLevel;
    protected double moneyAmount;
    protected long lastTaxPayDate;
    protected double taxFee;
    private final BlockLocation centerLocation;
    private final CityInventory cityInventory;
    private final CityStore cityStore;

    /*
     * Run time variables
     */
    protected OfflinePlayer cityManager;
    protected HashSet<OfflinePlayer> cityBuilders = new HashSet<>();
    protected DatabaseState databaseState; // shouldn't be "INSERT_TO_DATABASE"
    // "Protected" for saving
    protected final HashMap<BlockLocation, CityHouse> cityHouses = new HashMap<>();

    /*
     * COPIED FROM OLD CITY STRUCTURE
     *
     * Server taxes for cities will be:
     *      Total earnings since the last server fee date * serverTaxFee => retrieve the earnings (positive amounts) for this economy structure from history since the last server fee pay date
     *
     * World EconomyStructure:
     *      It'll go out from the server to the common players through job
     *      It'll go out from the city to the citizens through job (they'll have a bonus of exp and money)
     *     Every player earnings/expenses will include taxes (server's OR city's taxes; city taxes are higher)
     *
     *     City will get money to pay the default server tax and have some to upgrade or pay more jobs
     *     City expenses will not account on taxes (just earnings)
     *     All player expenses/earnings will have taxes either from city (it'll have a ceiling and floor, the manager will set the right amount for the city) or server (fixed one, lower than the city's floor tax)
     *          Earnings will be discounted the fee: receiver will receive the amount * fee and player amount * (1 - fee)
     *          Expenses will be increased the fee: receiver will pay amount * (1 + fee), receiver will receive the amount * fee
     *     Leaving/Creating/Entering the city will cost money (on server taxes)
     *     Updating the city won't cost taxes since it'll be used the city money (through earnings and deposits)
     *     Depositing to the city will be on server's taxes
     *
     * World without enough money:
     *      Players will receive less job money (server has 25% of his total capacity ? start correcting to economize) => total capacity being Amount per player * number of players
     *      Base fees will be higher
     *
     * City without enough money:
     *      Players will receive less job money (city has 25% of his level up capacity)
     *      If players cant lend the city money => City will be on negative and, eventually, break (after the 3 negatives spents on the server fee)
     */

    /**
     * Create the CityStructure from database information.
     *
     * @param cityId             city's id
     * @param cityName           city's name (will be lower cased)
     * @param cityLevel          city's level
     * @param moneyAmount        city's money amount (will be used to pay taxes, can be given to the players)
     * @param lastTaxPayDate     city's last time that paid taxes to the server
     * @param taxFee             city's tax fee
     * @param centerLocation     city's centre
     * @param cityInventoryItems city's shared inventory
     * @param cityStoreItems     city's store items
     * @throws IllegalStateException in case we can't
     */
    public CityStructure(int cityId, @NotNull String cityName, byte cityLevel, double moneyAmount,
            long lastTaxPayDate, double taxFee, @NotNull BlockLocation centerLocation,
            @Nullable ItemStack[] cityInventoryItems, @Nullable ItemStack[] cityStoreItems)
            throws IllegalStateException {
        this.cityId = cityId;
        this.cityName = cityName.toLowerCase();
        this.cityLevel = cityLevel > MAXIMUM_CITY_LEVEL ? MAXIMUM_CITY_LEVEL : cityLevel; // min(cityLevel, MAXIMUM_CITY_LEVEL)
        this.moneyAmount = moneyAmount;
        this.lastTaxPayDate = lastTaxPayDate;
        this.taxFee = taxFee;
        this.centerLocation = centerLocation;
        this.cityInventory = new CityInventory(InventoryHolder.mergeItems(cityInventoryItems));
        this.cityStore = new CityStore(InventoryHolder.mergeItems(cityStoreItems));

        // Check if city exceeded occupations
        for (OfflinePlayer offlinePlayer : getOfflinePlayers()) {
            if (offlinePlayer.getCityOccupation() == CityOccupation.BUILDER) {
                if (this.cityBuilders.size() < getMaximumNumberOfBuilders()) {
                    this.cityBuilders.add(offlinePlayer);
                } else {
                    ChangeOccupationResponse result;
                    // Change player occupation to citizen, exceeded number of builders
                    if ((result = changeOccupation(offlinePlayer,
                            CityOccupation.CITIZEN)) != ChangeOccupationResponse.SUCCESSFULLY_CHANGED)
                        throw new IllegalStateException(Util.appendStrings(
                                "Couldn't remove exceeding builder (playerId=", offlinePlayer.getPlayerId(),
                                ") from city (cityId=", cityId, "): ", result.name()));
                }
            } else if (offlinePlayer.getCityOccupation() == CityOccupation.MANAGER) {
                if (this.cityManager == null) {
                    this.cityManager = offlinePlayer;
                } else {
                    ChangeOccupationResponse result;
                    // Change player occupation to citizen, there must only be ONE manager
                    if ((result = changeOccupation(offlinePlayer,
                            CityOccupation.CITIZEN)) != ChangeOccupationResponse.SUCCESSFULLY_CHANGED)
                        throw new IllegalStateException(Util.appendStrings(
                                "Couldn't remove second manager (playerId=", offlinePlayer.getPlayerId(),
                                ") from city (cityId=", cityId, "): ", result.name()));
                }
            }
        }

        // Set database state
        this.databaseState = DatabaseState.ON_DATABASE;
    }

    /*
     * House handling
     */

    /**
     * Create a house for the citizens.<br>
     * Note: this should run asynchronously.
     *
     * @param blockLocation house location given by the city manager
     * @return a response for the CommandSender
     */
    public HouseCreationResponse createHouse(@NotNull final BlockLocation blockLocation) throws SQLException {
        // Check if there are more houses than current number of citizens + 1 (if level isn't the maximum)
        if (cityHouses.size() >= getMaximumNumberOfCitizens() + (cityLevel == MAXIMUM_CITY_LEVEL ? 0 : 2))
            return HouseCreationResponse.TOO_MANY_HOUSES_REGISTERED;

        // Check minimum height
        if (blockLocation.getY()
                - BlockProtectionType.CITY_HOUSES.getProtectionDistance() < WorldService.MINIMUM_PROTECTION_HEIGHT)
            return HouseCreationResponse.HOUSE_COORDINATE_Y_TOO_LOW;

        // Check minimum and maximum distance between city center
        // Note: this should use the protection distance of the CITY_BLOCKS because this would make the center on the "same height" as the block if checkY is enabled on
        // CITY_HOUSES but not on CITY_BLOCKS
        // Note: the corner of the house should be the corner of current protection range
        if (BlockProtectionType.CITY_BLOCKS.protectionDistanceSquared(blockLocation,
                centerLocation) < getProtectionRangeSquared()
                        - BlockProtectionType.CITY_HOUSES.getProtectionDistanceSquared())
            return HouseCreationResponse.TOO_FAR_FROM_CENTER;
        else if (BlockProtectionType.CITY_BLOCKS.protectionDistanceSquared(blockLocation,
                centerLocation) < BlockProtectionType.CITY_HOUSES.getProtectionDistanceSquared())
            return HouseCreationResponse.TOO_CLOSE_TO_THE_CENTER;

        // Check minimum distance between other houses
        for (BlockLocation existingBlockLocation : cityHouses.keySet())
            if (BlockProtectionType.CITY_HOUSES.protectionDistanceSquared(blockLocation,
                    existingBlockLocation) <= BlockProtectionType.CITY_HOUSES.getProtectionDistanceSquared())
                return HouseCreationResponse.TOO_CLOSE_TO_OTHER_HOUSE;

        int houseId;
        // Insert to database
        {
            Connection connection = LobsterCraft.dataSource.getConnection();
            // Prepare statement
            PreparedStatement preparedStatement = connection.prepareStatement(
                    "INSERT INTO `minecraft`.`city_house_locations` (`city_cityId`, `worlds_worldId`, `centerChunkX`, `centerChunkZ`, `centerX`, `centerY`, `centerZ`) "
                            + "VALUES (?, ?, ?, ?, ?, ?, ?);",
                    Statement.RETURN_GENERATED_KEYS);

            // Set variables
            preparedStatement.setInt(1, cityId);
            preparedStatement.setByte(2, blockLocation.getChunkLocation().getWorldId());
            preparedStatement.setInt(3, blockLocation.getChunkLocation().getChunkX());
            preparedStatement.setInt(4, blockLocation.getChunkLocation().getChunkZ());
            preparedStatement.setByte(5, blockLocation.getRelativeX());
            preparedStatement.setShort(6, blockLocation.getY());
            preparedStatement.setByte(7, blockLocation.getRelativeZ());

            // Execute statement, get generated key
            preparedStatement.execute();
            ResultSet generatedKeys = preparedStatement.getGeneratedKeys();

            // Check if id exists
            if (!generatedKeys.next())
                throw new SQLException("Generated key not generated!");

            // Get house key
            houseId = generatedKeys.getInt("houseId");
            if (houseId <= 0)
                throw new SQLException("House id must be greater than 0");
        }

        // Create variable
        CityHouse cityHouse = new CityHouse(houseId, cityId, blockLocation);

        // Insert house and return
        cityHouses.put(cityHouse, cityHouse);
        return HouseCreationResponse.SUCCESSFULLY_CREATED_HOUSE;
    }

    /**
     * Deletes house from database and from the city. This <b>CAN'T BE</b> recovered, all blocks <b>WILL be deleted</b> even when re-created.
     *
     * @param house house instance
     * @return true if house was successfully removed
     */
    public boolean deleteHouse(@NotNull final CityHouse house) {
        if (cityHouses.remove(house, house)) {
            // Add house to soon-to-be excluded list (this will delete house and its blocks during shutdown)
            LobsterCraft.servicesManager.cityService.deletedHouses.add(house.getHouseId());
            return true;
        }
        return false;
    }

    /*
     * Citizen handling
     */

    public Set<OfflinePlayer> getOfflinePlayers() {
        return LobsterCraft.servicesManager.playerHandlerService.getOfflinePlayersPlayersForCity(cityId);
    }

    public Set<OnlinePlayer> getOnlinePlayers(@Nullable OnlinePlayer.OnlineState onlineState) {
        return LobsterCraft.servicesManager.playerHandlerService.getOnlinePlayersForCity(cityId, onlineState);
    }

    /**
     * @param playerId player's id
     * @return the player's house id. Null if none
     */
    public CityHouse getHouseFromPlayer(int playerId) {
        for (CityHouse cityHouse : cityHouses.values())
            if (cityHouse.getPlayerId() != null && cityHouse.getPlayerId() == playerId)
                return cityHouse;
        return null;
    }

    public void broadcastMessage(@Nullable OnlinePlayer.OnlineState onlineState, @NotNull final String string) {
        for (OnlinePlayer onlinePlayer : getOnlinePlayers(onlineState)) {
            onlinePlayer.getPlayer().sendMessage(string);
        }
    }

    /**
     * Insert player on the city
     *
     * @param offlinePlayer player to join the city
     * @return a response for the CommandSender
     */
    public JoinCityResponse joinCity(@NotNull final OfflinePlayer offlinePlayer) {
        // Check if player is registered
        if (!offlinePlayer.isRegistered())
            return JoinCityResponse.PLAYER_NOT_REGISTERED;

        // Check if player is already a citizen
        if (offlinePlayer.getCityId() != null)
            return JoinCityResponse.ALREADY_CITIZEN;

        // Check if city has empty "slots"
        if (getOfflinePlayers().size() >= getMaximumNumberOfCitizens())
            return JoinCityResponse.CITY_OVERPOPULATED;

        // Call event
        PlayerJoinsCityEvent playerJoinsCityEvent = new PlayerJoinsCityEvent(offlinePlayer, this);
        // Note: this will, on monitor priority, if everything is succeeded, change offlinePlayer's attribute, so we MUST NOT change anything after this call:
        Bukkit.getPluginManager().callEvent(playerJoinsCityEvent);
        // DO NOTHING HERE THAT MIGHT CHANGE THE RESULT
        return playerJoinsCityEvent.getResult();
    }

    /**
     * Change player's occupation on the city. If player is becoming a MANAGER, the previous manager (if any) will be set to CITIZEN.
     *
     * @param offlinePlayer player to change occupation
     * @param occupation    player's future occupation
     * @return a response to the CommandSender
     */
    public ChangeOccupationResponse changeOccupation(@NotNull final OfflinePlayer offlinePlayer,
            @NotNull final CityOccupation occupation) {
        // Check if player is from this city
        if (offlinePlayer.getCityId() == null || offlinePlayer.getCityId() != cityId)
            return ChangeOccupationResponse.PLAYER_FROM_ANOTHER_CITY;

        // Check if player is becoming a BUILDER
        if (occupation == CityOccupation.BUILDER && cityBuilders.size() >= getMaximumNumberOfBuilders())
            return ChangeOccupationResponse.TOO_MANY_BUILDERS;

        // Check if player is leaving manager
        if (offlinePlayer.getCityOccupation() == CityOccupation.MANAGER)
            return ChangeOccupationResponse.CANT_CHANGE_FROM_MANAGER;

        // Check if a new player is joining manager position
        if (occupation == CityOccupation.MANAGER && this.cityManager != null)
            // Change previous manager's occupation to CITIZEN
            if (changeOccupation(this.cityManager,
                    CityOccupation.CITIZEN) != ChangeOccupationResponse.SUCCESSFULLY_CHANGED)
                return ChangeOccupationResponse.CANT_RECOVER_MANAGER_POSITION;

        // Call event
        PlayerChangesCityOccupationEvent changesCityOccupationEvent = new PlayerChangesCityOccupationEvent(
                offlinePlayer, this, occupation);
        // Note: this will, on monitor priority, if everything is succeeded, change offlinePlayer's attribute, so we MUST NOT change anything after this call:
        Bukkit.getPluginManager().callEvent(changesCityOccupationEvent);

        // DO NOTHING HERE THAT MIGHT CHANGE THE RESULT
        if (changesCityOccupationEvent.getResult() == ChangeOccupationResponse.SUCCESSFULLY_CHANGED) {
            if (occupation == CityOccupation.MANAGER)
                this.cityManager = offlinePlayer;
            else if (occupation == CityOccupation.BUILDER)
                this.cityBuilders.add(offlinePlayer);
        }

        return changesCityOccupationEvent.getResult();
    }

    /**
     * Change player's house.
     *
     * @param offlinePlayer player to change occupation
     * @param house         player selected house
     * @return a response to the CommandSender
     */
    public JoinHouseResponse joinHouse(@NotNull final OfflinePlayer offlinePlayer,
            @Nullable final CityHouse house) {
        // Check if player is from this city
        if (offlinePlayer.getCityId() == null || offlinePlayer.getCityId() != cityId)
            return JoinHouseResponse.PLAYER_FROM_ANOTHER_CITY;

        // Check if player already has a house
        if (house != null) {
            for (CityHouse cityHouse : cityHouses.values())
                if (cityHouse.getPlayerId() != null
                        && Objects.equals(cityHouse.getPlayerId(), offlinePlayer.getPlayerId()))
                    return JoinHouseResponse.ALREADY_HAVE_A_HOUSE;

            // Check if house is occupied
            if (house.getPlayerId() != null)
                return JoinHouseResponse.HOUSE_ALREADY_OCCUPIED;

            // Change owner
            house.playerId = null;
            house.databaseState = DatabaseState.UPDATE_DATABASE;
            return JoinHouseResponse.SUCCESSFULLY_CHANGED_HOUSE;
        } else {
            for (CityHouse cityHouse : cityHouses.values())
                if (cityHouse.getPlayerId() != null
                        && Objects.equals(cityHouse.getPlayerId(), offlinePlayer.getPlayerId())) {
                    // Change owner
                    cityHouse.playerId = null;
                    cityHouse.databaseState = DatabaseState.UPDATE_DATABASE;
                    return JoinHouseResponse.SUCCESSFULLY_CHANGED_HOUSE;
                }
            return JoinHouseResponse.DOES_NOT_HAVE_A_HOUSE;
        }
    }

    /*
     * Leveling
     */

    public int getMaximumNumberOfCitizens() {
        return INITIAL_NUMBER_OF_CITIZENS + NumberConversions.ceil(NUMBER_OF_CITIZENS_PER_LEVEL * (cityLevel - 1));
    }

    public int getNumberOfStoreItems() {
        return Math.min(
                INITIAL_NUMBER_OF_ITEMS_STORE
                        + NumberConversions.ceil(NUMBER_OF_ITEMS_STORE_PER_LEVEL * (cityLevel - 1)),
                MAXIMUM_NUMBER_OF_ITEMS_STORE);
    }

    public int getNumberOfInventoryItems() {
        return Math.min(
                INITIAL_NUMBER_OF_ITEMS_INVENTORY
                        + NumberConversions.ceil(NUMBER_OF_ITEMS_INVENTORY_PER_LEVEL * (cityLevel - 1)),
                MAXIMUM_NUMBER_OF_ITEMS_INVENTORY);
    }

    public int getMaximumNumberOfBuilders() {
        return NumberConversions.ceil(getMaximumNumberOfCitizens() * BUILDER_MAXIMUM_RATIO);
    }

    public double getProtectionRange() {
        return BlockProtectionType.CITY_BLOCKS.getProtectionRange(cityLevel);
    }

    public double getProtectionRangeSquared() {
        return BlockProtectionType.CITY_BLOCKS.getProtectionRangeSquared(cityLevel);
    }

    public double getLevelingCost() {
        return INITIAL_UPGRADE_COST * Math.pow(FACTOR_OF_INCREASE_OF_COST, (cityLevel - 1));
    }

    /*
     * Money handling
     */

    public TaxChangeResponse setTaxFee(double taxFee) {
        if (taxFee > MAXIMUM_TAX)
            return TaxChangeResponse.TAX_TOO_HIGH;
        else if (taxFee < MINIMUM_TAX)
            return TaxChangeResponse.TAX_TOO_LOW;

        this.databaseState = DatabaseState.UPDATE_DATABASE;
        broadcastMessage(OnlinePlayer.OnlineState.LOGGED_IN, Util.appendStrings("6As taxas mudaram de c",
                Util.formatTaxes(this.taxFee), "6 para c", Util.formatTaxes(taxFee)));
        this.taxFee = taxFee;
        return TaxChangeResponse.TAX_CHANGED;
    }

    /*
     * Getters
     */

    public int getCityId() {
        return cityId;
    }

    public String getCityName() {
        return cityName;
    }

    public BlockLocation getCenterLocation() {
        return centerLocation;
    }

    public CityInventory getCityInventory() {
        return cityInventory;
    }

    public CityStore getCityStore() {
        return cityStore;
    }

    /*
     * Overridden methods
     */

    @Override
    public boolean equals(Object obj) {
        return obj != null && obj instanceof CityStructure && ((CityStructure) obj).getCityId() == cityId;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(7, 21).append(cityId).append(cityName).toHashCode();
    }

    /*
     * Utility
     */

    private static ItemStack[] getItemStackArray(@NotNull final Map<ItemStack, Integer> items) {
        ItemStack[] itemArray = new ItemStack[items.size()];

        int index = 0;
        // Iterate through all items
        for (Map.Entry<ItemStack, Integer> entry : items.entrySet()) {
            ItemStack itemStackClone = entry.getKey().clone();
            // Set item amount
            itemStackClone.setAmount(entry.getValue());
            // Store item on array
            itemArray[index] = itemStackClone;
            index++;
        }

        return itemArray;
    }

    /*
     * Some classes
     */

    public enum HouseCreationResponse {
        SUCCESSFULLY_CREATED_HOUSE, HOUSE_COORDINATE_Y_TOO_LOW, TOO_MANY_HOUSES_REGISTERED, TOO_FAR_FROM_CENTER, TOO_CLOSE_TO_THE_CENTER, TOO_CLOSE_TO_OTHER_HOUSE
    }

    public enum JoinCityResponse {
        SUCCESSFULLY_JOINED_CITY, PLAYER_NOT_REGISTERED, CITY_OVERPOPULATED, ALREADY_CITIZEN
    }

    public enum ChangeOccupationResponse {
        SUCCESSFULLY_CHANGED, TOO_MANY_BUILDERS, CANT_CHANGE_FROM_MANAGER, PLAYER_FROM_ANOTHER_CITY, CANT_RECOVER_MANAGER_POSITION
    }

    public enum JoinHouseResponse {
        SUCCESSFULLY_CHANGED_HOUSE, PLAYER_FROM_ANOTHER_CITY, ALREADY_HAVE_A_HOUSE, DOES_NOT_HAVE_A_HOUSE, HOUSE_ALREADY_OCCUPIED
    }

    public enum TaxChangeResponse {
        TAX_TOO_HIGH, TAX_TOO_LOW, TAX_CHANGED
    }

    private class InventoryClickHandler implements InventoryHolder.ClickEventHandler {

        @Override
        public void onOptionClick(InventoryHolder.OptionClickEvent event) {
            InventoryClickEvent clickEvent = event.getBukkitClickEvent();
            // Limit insertions by current city maximum
        }
    }

    private class StoreClickHandler implements InventoryHolder.ClickEventHandler {

        @Override
        public void onOptionClick(InventoryHolder.OptionClickEvent event) {
            InventoryClickEvent clickEvent = event.getBukkitClickEvent();
            // Limit insertions by current city maximum
        }
    }

    protected class CityInventory extends InventoryHolder {

        protected final HashMap<ItemStack, Integer> items;

        /**
         * @param items an map with merged ItemStacks
         * @see InventoryHolder#mergeItems(ItemStack[])
         */
        public CityInventory(HashMap<ItemStack, Integer> items) {
            super(Util.appendStrings(cityName, " - inventario p.%page%"), new InventoryClickHandler(),
                    MAXIMUM_NUMBER_OF_ITEMS_INVENTORY);
            this.items = items;
        }

        public ItemStack[] getItemStackArray() {
            return CityStructure.getItemStackArray(items);
        }
    }

    protected class CityStore extends InventoryHolder {

        protected final HashMap<ItemStack, Integer> items;

        /**
         * @param items an map with merged ItemStacks
         * @see InventoryHolder#mergeItems(ItemStack[])
         */
        public CityStore(HashMap<ItemStack, Integer> items) {
            super(Util.appendStrings(cityName, " - loja p.%page%"), new StoreClickHandler(),
                    MAXIMUM_NUMBER_OF_ITEMS_STORE);
            this.items = items;
        }

        public ItemStack[] getItemStackArray() {
            return CityStructure.getItemStackArray(items);
        }
    }

    public static class CityHouse extends BlockLocation {

        // Database primary key
        private final int houseId;
        private final int cityId;

        // Other variables
        protected Integer playerId;
        protected DatabaseState databaseState; // Can't be "INSERT_TO_DATABASE"

        public CityHouse(int houseId, short cityId, @Nullable Integer playerId,
                @NotNull ChunkLocation chunkLocation, byte x, short y, byte z) {
            super(chunkLocation, x, y, z);
            this.houseId = houseId;
            this.cityId = cityId;

            this.playerId = playerId;
            this.databaseState = DatabaseState.ON_DATABASE;
        }

        public CityHouse(int houseId, int cityId, @NotNull BlockLocation blockLocation) {
            super(blockLocation);
            this.houseId = houseId;
            this.cityId = cityId;

            this.databaseState = DatabaseState.ON_DATABASE;
        }

        public int getHouseId() {
            return houseId;
        }

        public Integer getPlayerId() {
            return playerId;
        }

        public int getCityId() {
            return cityId;
        }

        public CityStructure getCityStructure() {
            return LobsterCraft.servicesManager.cityService.getCity(cityId);
        }
    }
}