/* * 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 silentium.gameserver; import java.io.File; import java.util.Arrays; import java.util.List; import java.util.Map; import javolution.util.FastList; import javolution.util.FastMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import silentium.commons.utils.Rnd; import silentium.gameserver.configs.MainConfig; import silentium.gameserver.configs.PlayersConfig; import silentium.gameserver.data.xml.parsers.XMLDocumentFactory; import silentium.gameserver.model.L2ItemInstance; import silentium.gameserver.model.L2ManufactureItem; import silentium.gameserver.model.L2RecipeInstance; import silentium.gameserver.model.L2RecipeList; import silentium.gameserver.model.L2Skill; import silentium.gameserver.model.actor.instance.L2PcInstance; import silentium.gameserver.model.itemcontainer.Inventory; import silentium.gameserver.network.SystemMessageId; import silentium.gameserver.network.serverpackets.ActionFailed; import silentium.gameserver.network.serverpackets.ItemList; import silentium.gameserver.network.serverpackets.RecipeBookItemList; import silentium.gameserver.network.serverpackets.RecipeItemMakeInfo; import silentium.gameserver.network.serverpackets.RecipeShopItemInfo; import silentium.gameserver.network.serverpackets.StatusUpdate; import silentium.gameserver.network.serverpackets.SystemMessage; import silentium.gameserver.taskmanager.AttackStanceTaskManager; import silentium.gameserver.utils.Util; public class RecipeController { protected static final Logger _log = LoggerFactory.getLogger(RecipeController.class.getName()); private final Map<Integer, L2RecipeList> _lists = new FastMap<>(); protected static final Map<Integer, RecipeItemMaker> _activeMakers = new FastMap<Integer, RecipeItemMaker>().shared(); public static RecipeController getInstance() { return SingletonHolder._instance; } protected RecipeController() { try { loadFromXML(); _log.info("RecipeController: Loaded " + _lists.size() + " recipes."); } catch (Exception e) { _log.error("RecipeController: Failed loading recipe list", e); } } public int getRecipesCount() { return _lists.size(); } public L2RecipeList getRecipeList(int listId) { return _lists.get(listId); } public L2RecipeList getRecipeByItemId(int itemId) { for (L2RecipeList find : _lists.values()) { if (find.getRecipeId() == itemId) return find; } return null; } public synchronized void requestBookOpen(L2PcInstance player, boolean isDwarvenCraft) { RecipeBookItemList response = new RecipeBookItemList(isDwarvenCraft, player.getMaxMp()); response.addRecipes(isDwarvenCraft ? player.getDwarvenRecipeBook() : player.getCommonRecipeBook()); player.sendPacket(response); return; } public synchronized void requestMakeItemAbort(L2PcInstance player) { _activeMakers.remove(player.getObjectId()); } public synchronized void requestManufactureItem(L2PcInstance manufacturer, int recipeListId, L2PcInstance player) { L2RecipeList recipeList = getValidRecipeList(player, recipeListId); if (recipeList == null) return; List<L2RecipeList> dwarfRecipes = Arrays.asList(manufacturer.getDwarvenRecipeBook()); List<L2RecipeList> commonRecipes = Arrays.asList(manufacturer.getCommonRecipeBook()); if (!dwarfRecipes.contains(recipeList) && !commonRecipes.contains(recipeList)) { Util.handleIllegalPlayerAction(player, player.getName() + " of account " + player.getAccountName() + " sent a false recipe id.", MainConfig.DEFAULT_PUNISH); return; } RecipeItemMaker maker = new RecipeItemMaker(manufacturer, recipeList, player); if (maker._isValid) maker.run(); } public synchronized void requestMakeItem(L2PcInstance player, int recipeListId) { if (AttackStanceTaskManager.getInstance().getAttackStanceTask(player) || player.isInDuel()) { player.sendPacket(SystemMessageId.CANT_OPERATE_PRIVATE_STORE_DURING_COMBAT); return; } L2RecipeList recipeList = getValidRecipeList(player, recipeListId); if (recipeList == null) return; List<L2RecipeList> dwarfRecipes = Arrays.asList(player.getDwarvenRecipeBook()); List<L2RecipeList> commonRecipes = Arrays.asList(player.getCommonRecipeBook()); if (!dwarfRecipes.contains(recipeList) && !commonRecipes.contains(recipeList)) { Util.handleIllegalPlayerAction(player, player.getName() + " of account " + player.getAccountName() + " sent a false recipe id.", MainConfig.DEFAULT_PUNISH); return; } RecipeItemMaker maker = new RecipeItemMaker(player, recipeList, player); if (maker._isValid) maker.run(); } private void loadFromXML() throws Exception { File file = new File(MainConfig.DATAPACK_ROOT + "/data/xml/recipes.xml"); final Document doc = XMLDocumentFactory.getInstance().loadDocument(file); List<L2RecipeInstance> recipePartList = new FastList<>(); String recipeName; int id; Node n = doc.getFirstChild(); for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { if ("item".equalsIgnoreCase(d.getNodeName())) { recipePartList.clear(); NamedNodeMap attrs = d.getAttributes(); Node att; att = attrs.getNamedItem("id"); if (att == null) { _log.error("Missing id for recipe item, skipping"); continue; } id = Integer.parseInt(att.getNodeValue()); att = attrs.getNamedItem("name"); if (att == null) { _log.error("Missing name for recipe item id: " + id + ", skipping"); continue; } recipeName = att.getNodeValue(); int recipeId = -1; int level = -1; boolean isDwarvenRecipe = true; int mpCost = -1; int successRate = -1; int prodId = -1; int count = -1; for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { if ("recipe".equalsIgnoreCase(c.getNodeName())) { NamedNodeMap atts = c.getAttributes(); recipeId = Integer.parseInt(atts.getNamedItem("id").getNodeValue()); level = Integer.parseInt(atts.getNamedItem("level").getNodeValue()); isDwarvenRecipe = atts.getNamedItem("type").getNodeValue().equalsIgnoreCase("dwarven"); } else if ("mpCost".equalsIgnoreCase(c.getNodeName())) { mpCost = Integer.parseInt(c.getTextContent()); } else if ("successRate".equalsIgnoreCase(c.getNodeName())) { successRate = Integer.parseInt(c.getTextContent()); } else if ("ingredient".equalsIgnoreCase(c.getNodeName())) { int ingId = Integer.parseInt(c.getAttributes().getNamedItem("id").getNodeValue()); int ingCount = Integer.parseInt(c.getAttributes().getNamedItem("count").getNodeValue()); recipePartList.add(new L2RecipeInstance(ingId, ingCount)); } else if ("production".equalsIgnoreCase(c.getNodeName())) { prodId = Integer.parseInt(c.getAttributes().getNamedItem("id").getNodeValue()); count = Integer.parseInt(c.getAttributes().getNamedItem("count").getNodeValue()); } } L2RecipeList recipeList = new L2RecipeList(id, level, recipeId, recipeName, successRate, mpCost, prodId, count, isDwarvenRecipe); for (L2RecipeInstance recipePart : recipePartList) recipeList.addRecipe(recipePart); _lists.put(_lists.size(), recipeList); } } } private class RecipeItemMaker implements Runnable { protected boolean _isValid; protected List<TempItem> _items = null; protected final L2RecipeList _recipeList; protected final L2PcInstance _player; // "crafter" protected final L2PcInstance _target; // "customer" protected final int _skillId; protected final int _skillLevel; protected double _manaRequired; protected int _price; protected int _totalItems; protected int _materialsRefPrice; public RecipeItemMaker(L2PcInstance pPlayer, L2RecipeList pRecipeList, L2PcInstance pTarget) { _player = pPlayer; _target = pTarget; _recipeList = pRecipeList; _isValid = false; _skillId = _recipeList.isDwarvenRecipe() ? L2Skill.SKILL_CREATE_DWARVEN : L2Skill.SKILL_CREATE_COMMON; _skillLevel = _player.getSkillLevel(_skillId); _manaRequired = _recipeList.getMpCost(); _player.isInCraftMode(true); if (_player.isAlikeDead() || _target.isAlikeDead()) { _player.sendPacket(ActionFailed.STATIC_PACKET); abort(); return; } if (_player.isProcessingTransaction() || _target.isProcessingTransaction()) { _target.sendPacket(ActionFailed.STATIC_PACKET); abort(); return; } // validate recipe list if (_recipeList.getRecipes().length == 0) { _player.sendPacket(ActionFailed.STATIC_PACKET); abort(); return; } // validate skill level if (_recipeList.getLevel() > _skillLevel) { _player.sendPacket(ActionFailed.STATIC_PACKET); abort(); return; } // check that customer can afford to pay for creation services if (_player != _target) { for (L2ManufactureItem temp : _player.getCreateList().getList()) { if (temp.getRecipeId() == _recipeList.getId()) // find recipe for item we want manufactured { _price = temp.getCost(); if (_target.getAdena() < _price) // check price { _target.sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); abort(); return; } break; } } } // make temporary items if ((_items = listItems(false)) == null) { abort(); return; } // calculate reference price for (TempItem i : _items) { _materialsRefPrice += i.getReferencePrice() * i.getQuantity(); _totalItems += i.getQuantity(); } // initial mana check requires MP as written on recipe if (_player.getCurrentMp() < _manaRequired) { _target.sendPacket(SystemMessageId.NOT_ENOUGH_MP); abort(); return; } updateMakeInfo(true); updateCurMp(); updateCurLoad(); _player.isInCraftMode(false); _isValid = true; } @Override public void run() { if (!PlayersConfig.IS_CRAFTING_ENABLED) { _target.sendMessage("Item creation is currently disabled."); abort(); return; } if (_player == null || _target == null) { _log.warn("Player or target == null (disconnected?), aborting" + _target + _player); abort(); return; } if (!_player.isOnline() || !_target.isOnline()) { _log.warn("Player or target is not online, aborting " + _target + _player); abort(); return; } _player.reduceCurrentMp(_manaRequired); // first take adena for manufacture if ((_target != _player) && _price > 0) // customer must pay for services { // attempt to pay for item L2ItemInstance adenatransfer = _target.transferItem("PayManufacture", _target.getInventory().getAdenaInstance().getObjectId(), _price, _player.getInventory(), _player); if (adenatransfer == null) { _target.sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); abort(); return; } } if ((_items = listItems(true)) == null) // this line actually takes materials from inventory { // handle possible cheaters here (they click craft then try to get rid of items in order to get free craft) } else if (Rnd.get(100) < _recipeList.getSuccessRate()) { rewardPlayer(); // and immediately puts created item in its place updateMakeInfo(true); } else { if (_target != _player) { SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.CREATION_OF_S2_FOR_S1_AT_S3_ADENA_FAILED); msg.addPcName(_target); msg.addItemName(_recipeList.getItemId()); msg.addItemNumber(_price); _player.sendPacket(msg); msg = SystemMessage.getSystemMessage(SystemMessageId.S1_FAILED_TO_CREATE_S2_FOR_S3_ADENA); msg.addPcName(_player); msg.addItemName(_recipeList.getItemId()); msg.addItemNumber(_price); _target.sendPacket(msg); } else _target.sendPacket(SystemMessageId.ITEM_MIXING_FAILED); updateMakeInfo(false); } // update load and mana bar of craft window updateCurMp(); updateCurLoad(); _activeMakers.remove(_player.getObjectId()); _player.isInCraftMode(false); _target.sendPacket(new ItemList(_target, false)); } private void updateMakeInfo(boolean success) { if (_target == _player) _target.sendPacket(new RecipeItemMakeInfo(_recipeList.getId(), _target, success)); else _target.sendPacket(new RecipeShopItemInfo(_player, _recipeList.getId())); } private void updateCurLoad() { StatusUpdate su = new StatusUpdate(_target); su.addAttribute(StatusUpdate.CUR_LOAD, _target.getCurrentLoad()); _target.sendPacket(su); } private void updateCurMp() { StatusUpdate su = new StatusUpdate(_target); su.addAttribute(StatusUpdate.CUR_MP, (int) _target.getCurrentMp()); _target.sendPacket(su); } private List<TempItem> listItems(boolean remove) { L2RecipeInstance[] recipes = _recipeList.getRecipes(); Inventory inv = _target.getInventory(); List<TempItem> materials = new FastList<TempItem>(); SystemMessage sm; for (L2RecipeInstance recipe : recipes) { int quantity = _recipeList.isConsumable() ? (int) (recipe.getQuantity() * MainConfig.RATE_CONSUMABLE_COST) : (int) recipe.getQuantity(); if (quantity > 0) { L2ItemInstance item = inv.getItemByItemId(recipe.getItemId()); int itemQuantityAmount = item == null ? 0 : item.getCount(); // check materials if (itemQuantityAmount < quantity) { sm = SystemMessage.getSystemMessage(SystemMessageId.MISSING_S2_S1_TO_CREATE); sm.addItemName(recipe.getItemId()); sm.addItemNumber(quantity - itemQuantityAmount); _target.sendPacket(sm); abort(); return null; } // make new temporary object, just for counting puroses TempItem temp = new TempItem(item, quantity); materials.add(temp); } } if (remove) { for (TempItem tmp : materials) { inv.destroyItemByItemId("Manufacture", tmp.getItemId(), tmp.getQuantity(), _target, _player); if (tmp.getQuantity() > 1) { sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); sm.addItemName(tmp.getItemId()); sm.addItemNumber(tmp.getQuantity()); _target.sendPacket(sm); } else { sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); sm.addItemName(tmp.getItemId()); _target.sendPacket(sm); } } } return materials; } private void abort() { updateMakeInfo(false); _player.isInCraftMode(false); _activeMakers.remove(_player.getObjectId()); } /** * For item counting or checking purposes. When you don't want to modify inventory class contains itemId, quantity, ownerId, * referencePrice, but not objectId */ private class TempItem { // no object id stored, this will be only "list" of items with it's owner private final int _itemId; private final int _quantity; private final int _referencePrice; public TempItem(L2ItemInstance item, int quantity) { super(); _itemId = item.getItemId(); _quantity = quantity; _referencePrice = item.getReferencePrice(); } public int getQuantity() { return _quantity; } public int getReferencePrice() { return _referencePrice; } public int getItemId() { return _itemId; } } private void rewardPlayer() { int itemId = _recipeList.getItemId(); int itemCount = _recipeList.getCount(); _target.getInventory().addItem("Manufacture", itemId, itemCount, _target, _player); // inform customer of earned item SystemMessage sm = null; if (_target != _player) { // inform manufacturer of earned profit if (itemCount == 1) { sm = SystemMessage.getSystemMessage(SystemMessageId.S2_CREATED_FOR_S1_FOR_S3_ADENA); sm.addString(_target.getName()); sm.addItemName(itemId); sm.addItemNumber(_price); _player.sendPacket(sm); sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CREATED_S2_FOR_S3_ADENA); sm.addString(_player.getName()); sm.addItemName(itemId); sm.addItemNumber(_price); _target.sendPacket(sm); } else { sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S3_S_CREATED_FOR_S1_FOR_S4_ADENA); sm.addString(_target.getName()); sm.addNumber(itemCount); sm.addItemName(itemId); sm.addItemNumber(_price); _player.sendPacket(sm); sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CREATED_S2_S3_S_FOR_S4_ADENA); sm.addString(_player.getName()); sm.addNumber(itemCount); sm.addItemName(itemId); sm.addItemNumber(_price); _target.sendPacket(sm); } } if (itemCount > 1) { sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_S2_S1_S); sm.addItemName(itemId); sm.addNumber(itemCount); _target.sendPacket(sm); } else { sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_ITEM_S1); sm.addItemName(itemId); _target.sendPacket(sm); } updateMakeInfo(true); // success } } private L2RecipeList getValidRecipeList(L2PcInstance player, int id) { L2RecipeList recipeList = getRecipeList(id - 1); if ((recipeList == null) || (recipeList.getRecipes().length == 0)) { player.sendMessage("No recipe for: " + id); player.isInCraftMode(false); return null; } return recipeList; } private static class SingletonHolder { protected static final RecipeController _instance = new RecipeController(); } }