/* * 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 2, 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. * * http://www.gnu.org/copyleft/gpl.html */ package net.sf.l2j.gameserver; ///// //// /// // This file is now overcomented to explain all the changes // in detail. Feel free to remove them later /// //// ///// import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.LineNumberReader; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import java.util.StringTokenizer; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javolution.util.FastList; import javolution.util.FastMap; import net.sf.l2j.Config; import net.sf.l2j.gameserver.model.Inventory; import net.sf.l2j.gameserver.model.L2ItemInstance; import net.sf.l2j.gameserver.model.L2ManufactureItem; import net.sf.l2j.gameserver.model.L2RecipeInstance; import net.sf.l2j.gameserver.model.L2RecipeList; import net.sf.l2j.gameserver.model.L2Skill; import net.sf.l2j.gameserver.model.actor.instance.L2PcInstance; import net.sf.l2j.gameserver.serverpackets.ActionFailed; import net.sf.l2j.gameserver.serverpackets.InventoryUpdate; import net.sf.l2j.gameserver.serverpackets.ItemList; import net.sf.l2j.gameserver.serverpackets.MagicSkillUser; import net.sf.l2j.gameserver.serverpackets.RecipeBookItemList; import net.sf.l2j.gameserver.serverpackets.RecipeItemMakeInfo; import net.sf.l2j.gameserver.serverpackets.RecipeShopItemInfo; import net.sf.l2j.gameserver.serverpackets.SetupGauge; import net.sf.l2j.gameserver.serverpackets.StatusUpdate; import net.sf.l2j.gameserver.serverpackets.SystemMessage; import net.sf.l2j.gameserver.skills.Stats; public class RecipeController { protected static Logger _log = Logger.getLogger(RecipeController.class.getName()); private static RecipeController _instance; private Map _lists; protected static Map activeMakers = Collections.synchronizedMap(new WeakHashMap()); protected static final Random _rnd = new Random(); public static RecipeController getInstance() { if (_instance == null) { _instance = new RecipeController(); } return _instance; } public RecipeController() { _lists = new FastMap(); String line = null; LineNumberReader lnr = null; try { File recipesData = new File(Config.DATAPACK_ROOT, "data/recipes.csv"); lnr = new LineNumberReader(new BufferedReader( new FileReader(recipesData))); while ((line=lnr.readLine()) != null) { if (line.trim().length() == 0 || line.startsWith("#")) { continue; } parseList(line); } _log.config("RecipeController: Loaded "+_lists.size()+" Recipes."); } catch (Exception e) { if (lnr != null) { _log.log(Level.WARNING, "error while creating recipe controller in linenr: "+lnr.getLineNumber(), e); } else { _log.warning("No recipes were found in data folder"); } } finally { try { lnr.close(); } catch (Exception e) { } } } public int getRecipesCount() { return _lists.size(); } public L2RecipeList getRecipeList(int listId) { return _lists.get(listId); } public L2RecipeList getRecipeByItemId(int itemId) { for (int i = 0; i < _lists.size(); i++) { L2RecipeList find = _lists.get(new Integer(i)); if (find.getRecipeId() == itemId) { return find; } } return null; } public synchronized void requestBookOpen(L2PcInstance player, boolean isDwarvenCraft) { RecipeItemMaker maker=null; // @@todo: this must be removed, but also made sure it will happen only if realy crafting if (Config.ALT_GAME_CREATION) { maker = activeMakers.get(player); if(!player.isInCraftMode() && maker!=null) { activeMakers.remove(player); if (Config.DEBUG) _log.fine("removed "+player.getName()+" from activeMakers by force, trust only isInCraftMode for acurate state"); } } if (maker == null) { RecipeBookItemList response = new RecipeBookItemList(isDwarvenCraft,player.getMaxMp()); if (isDwarvenCraft) response.addRecipes(player.getDwarvenRecipeBook()); else response.addRecipes(player.getCommonRecipeBook()); player.sendPacket(response); return; } if (Config.DEBUG) _log.fine("requestBookOpen: Busy creating"); SystemMessage sm = new SystemMessage(SystemMessage.CANT_ALTER_RECIPEBOOK_WHILE_CRAFTING); sm.addItemName(maker.recipeList.getItemId()); player.sendPacket(sm); return; } public synchronized void requestManufactureItem(L2PcInstance manufacturer, int recipeListId, L2PcInstance customer) { L2RecipeList recipeList = getRecipeList(recipeListId-1); SystemMessage sm = new SystemMessage(614); if(manufacturer.isInCraftMode()) { if(Config.DEBUG) _log.fine("requestManufactureItem called but manufacturer is in craft mode"); sm = new SystemMessage(614); sm.addString("Manufacturer is busy."); sm.addItemName(recipeList.getItemId()); customer.sendPacket(sm); return; } if ((recipeList == null) || (recipeList.getRecipes().length == 0)) { _log.warning("I bet this can never happen normally, "+ "cause customer can order only listed recipes, which are 100% in manufacturer's recipe book"); sm = new SystemMessage(614); sm.addString("No recipe for: " + recipeListId); customer.sendPacket(sm); manufacturer.isInCraftMode(false); return; } if (Config.ALT_GAME_CREATION) { RecipeItemMaker maker = activeMakers.get(manufacturer); if (maker != null) { if(!manufacturer.isInCraftMode()) { if(Config.DEBUG) { _log.fine(maker.manufacturer.getName() + " is in activeMakers but not IsInCraftmode"); _log.fine("decision: isincraftmode() is right, activeMakers wrong, removing from activeMakers"); } activeMakers.remove(manufacturer); } else { sm = new SystemMessage(614); sm.addString("Manufacturer is busy."); sm.addItemName(recipeList.getItemId()); customer.sendPacket(sm); manufacturer.isInCraftMode(false); return; } } maker = new RecipeItemMaker(manufacturer, recipeList, customer); if (maker!=null && manufacturer!=null && maker.isValid) activeMakers.put(manufacturer, maker); } else // for regular crafting we don't use activeMakers new RecipeItemMaker(manufacturer, recipeList, customer); } public synchronized void requestMakeItem(L2PcInstance manufacturer, int recipeListId) { L2RecipeList recipeList = getRecipeList(recipeListId-1); SystemMessage sm = new SystemMessage(614); if ((recipeList == null) || (recipeList.getRecipes().length == 0)) { sm = new SystemMessage(614); sm.addString("No recipe for: " + recipeListId); manufacturer.sendPacket(sm); return; } if(manufacturer.isInCraftMode()) { sm = new SystemMessage(614); sm.addString("You are busy creating "); sm.addItemName(recipeList.getItemId()); manufacturer.sendPacket(sm); return; } if (Config.ALT_GAME_CREATION) { RecipeItemMaker maker = activeMakers.get(manufacturer); if (maker != null) activeMakers.remove(manufacturer); maker = new RecipeItemMaker(manufacturer, recipeList, null); if (manufacturer!=null && maker!=null && maker.isValid) activeMakers.put(manufacturer, maker); } else { new RecipeItemMaker(manufacturer, recipeList, null); } } public synchronized void requestMakeItemAbort(L2PcInstance manufacturer) { manufacturer.isInCraftMode(false); if (Config.DEBUG) _log.fine(manufacturer.getName()+" IsInCraftMode(false)"); if(Config.ALT_GAME_CREATION && activeMakers!=null) { if(activeMakers.get(manufacturer)!=null) activeMakers.remove(manufacturer); if (Config.DEBUG) _log.fine("requestMakeItemAbort called - removing " +manufacturer.getName() +" from activeMakers "); } } private void parseList(String line) { StringTokenizer st = new StringTokenizer(line,";"); List recipePartList = new FastList(); //we use common/dwarf for easy reading of the recipes.csv file String recipeTypeString = st.nextToken(); //now we parse the string into a boolean boolean isDwarvenRecipe; if (recipeTypeString.equalsIgnoreCase("dwarven")) isDwarvenRecipe = true; else if (recipeTypeString.equalsIgnoreCase("common")) isDwarvenRecipe = false; else { //prints a helpfull message _log.warning("Error parsing recipes.csv, unknow recipe type "+recipeTypeString); return; } String recipeName = st.nextToken(); int id = Integer.parseInt(st.nextToken()); int recipeId = Integer.parseInt(st.nextToken()); int level = Integer.parseInt(st.nextToken()); //material StringTokenizer st2 = new StringTokenizer(st.nextToken(), "[],"); while (st2.hasMoreTokens()) { StringTokenizer st3 = new StringTokenizer(st2.nextToken(), "()"); int rpItemId = Integer.parseInt(st3.nextToken()); int quantity = Integer.parseInt(st3.nextToken()); L2RecipeInstance rp = new L2RecipeInstance(rpItemId, quantity); recipePartList.add(rp); //recipe.addRecipe(rp); } int itemId = Integer.parseInt(st.nextToken()); int count = Integer.parseInt(st.nextToken()); //npc fee /*String notdoneyet = */ st.nextToken(); int mpCost = Integer.parseInt(st.nextToken()); int successRate = Integer.parseInt(st.nextToken()); L2RecipeList recipeList = new L2RecipeList(id,level,recipeId,recipeName,successRate,mpCost,itemId,count,isDwarvenRecipe); for (L2RecipeInstance recipePart : recipePartList) { recipeList.addRecipe(recipePart); } _lists.put(new Integer(_lists.size()), recipeList); } class RecipeItemMaker implements Runnable { int timeReady; final L2PcInstance manufacturer; final L2PcInstance customer; final L2RecipeList recipeList; final List items; int skillLevel; int creationPasses; int materialsRefPrice; int totalItems; int usedItems; int price; boolean isValid; RecipeItemMaker(L2PcInstance manufacturer, L2RecipeList recipeList, L2PcInstance customer ) { this.manufacturer = manufacturer; this.customer = customer; this.recipeList = recipeList; this.items = new FastList(); if(recipeList.isDwarvenRecipe()) this.skillLevel = manufacturer.getSkillLevel(L2Skill.SKILL_CREATE_DWARVEN); else this.skillLevel = manufacturer.getSkillLevel(L2Skill.SKILL_CREATE_COMMON); // crafting is disabled... if (!Config.IS_CRAFTING_ENABLED) { SystemMessage sm = new SystemMessage(614); sm.addString("Crafting is currently disabled."); if (customer == null) manufacturer.sendPacket(sm); else customer.sendPacket(sm); manufacturer.isInCraftMode(false); return; } if (manufacturer.isInCraftMode()) { if(Config.DEBUG) _log.fine("manufacturer.isInCraftMode()==true, in RecipeItemMaker constructor"); SystemMessage sm = new SystemMessage(614); if (customer == null) { sm.addString("You are busy crafting"); manufacturer.sendPacket(sm); } else { sm.addString("Manufacturer is busy crafting"); customer.sendPacket(sm); }; return; } if (skillLevel <= 0) { manufacturer.sendPacket(new ActionFailed()); manufacturer.isInCraftMode(false); if(customer!=null) { customer.sendPacket(new ActionFailed()); if(Config.DEBUG) _log.warning("skillLevel <= 0 and customer!=null, so why can manufacturer open manufacture shop if he has no skill?"); } return; } if ((recipeList == null) || (recipeList.getRecipes().length == 0)) { SystemMessage sm = new SystemMessage(614); sm.addString("No such recipe"); manufacturer.sendPacket(sm); if(customer!=null) { customer.sendPacket(sm); if(Config.DEBUG) _log.warning("customer!=null, so why can manufacturer offer manufacture for item if he has no recipe ?"); } return; } if (customer != null) { for (L2ManufactureItem temp : manufacturer.getCreateList().getList()) if (temp.getRecipeId() == recipeList.getId()) { price = temp.getCost(); if (customer.getAdena() < price) { customer.sendPacket(new SystemMessage(SystemMessage.YOU_NOT_ENOUGH_ADENA)); return; } break; } } if (recipeList.getLevel()>skillLevel) { SystemMessage sm = new SystemMessage(614); sm.addString("Need skill level "+recipeList.getLevel()); manufacturer.sendPacket(sm); return; } L2RecipeInstance[] recipes = recipeList.getRecipes(); Inventory inv = null; if (customer == null) inv = manufacturer.getInventory(); else inv = customer.getInventory(); for (L2RecipeInstance recipe : recipes) { if (recipe.getQuantity() != 0) { L2ItemInstance invItem = inv.getItemByItemId(recipe.getItemId()); if ((invItem == null) || (recipe.getQuantity()>invItem.getCount())) { SystemMessage sm = new SystemMessage(614); sm.addString("You dont have the right elements for making this item"); if (customer == null) manufacturer.sendPacket(sm); else customer.sendPacket(sm); return; } } } if (manufacturer.getCurrentMp() < recipeList.getMpCost()) { if (Config.DEBUG) _log.fine("not enough mana in RecipeItemMaker constructor"); SystemMessage sm = new SystemMessage(SystemMessage.NOT_ENOUGH_MP); if (customer == null) manufacturer.sendPacket(sm); else customer.sendPacket(sm); return; } creationPasses=0; for (L2RecipeInstance recipe : recipes) { if (recipe.getQuantity() != 0) { L2ItemInstance invItem = inv.getItemByItemId(recipe.getItemId()); invItem = inv.dropItem(invItem, recipe.getQuantity()); items.add(invItem); materialsRefPrice += invItem.getReferencePrice()*invItem.getCount(); totalItems += invItem.getCount(); creationPasses+=invItem.getCount()/skillLevel; if((invItem.getCount()%skillLevel)>0) creationPasses++; } } if(Config.DEBUG) { //_log.warning("calculated creation passes: "+creationPasses); if(creationPasses==0) { creationPasses++; _log.warning("creationPasses was ZERO so creationPasses++ and it is now " + creationPasses); } } isValid = true; manufacturer.isInCraftMode(true); // here we say craf mode started, no reason to do it sooner, just makes problems //ClientScheduler.scheduleCraftAction(this, 100); run(); // for not-alternative crafting -> no reason why not craft immediately, cause it uses really little cpu // and will never make lag ... scheduled things tend to make lag, things that are immediately done don't if you // have sufficient cpu anyway ... how long can craft take ? few nanoseconds ? // and for ALT_CRAFTING=true, first run is innitial, and takes no time also, and on end, if needed, another run // is scheduled } public void run() { if (!Config.ALT_GAME_CREATION || items.isEmpty()) // this method does the actual creation of item, { // it is called immediately for normal crafting, completeCreation(); // cause normal crafting does not take time, bug gives no XP/SP manufacturer.isInCraftMode(false); // for ALT_CRAFTING it is called only when all items are "equiped" // thru multiple scheduled runs using // ClientScheduler.ScheduleCraftAction() - separate thread pool // there should be no thread launching at all if(Config.DEBUG) { //_log.warning("MP_Cost: "+recipeList.getMpCost()+" "); //_log.warning("passes: "+creationPasses+" "); //_log.warning("mana/pass:"+recipeList.getMpCost()/creationPasses+" "); //_log.warning("error: "+(recipeList.getMpCost()-((recipeList.getMpCost()/creationPasses)*creationPasses))); } return; } // all this code is for ALT_GAME_CREATION only // previous code was very confusing, which led // to "craft lag" due to misunderstanding of // "crafting" in normal mode. // normal mode takes no time but gives no XP/SP // ALT_MODE takes time but gives XP/SP as reward // --------------------------------------------- // run() is scheduled multiple times, // (equiping) skillLevel items on every run, until // all items are "equiped", and then on next run, // completeCreation() will produce the result // and remove equiped materials from inventory // // on every run() part of mana is deducted from // manufacturer, amount is calculated as sum of // all item counts from recipe. double currentManufacturerMp = manufacturer.getCurrentMp(); // we need player to have only mana needed if (currentManufacturerMp < ((double)recipeList.getMpCost()/(double)creationPasses)) { // for this pass, next pass is in 1 second if (Config.DEBUG) _log.warning("not enough mana in run()"); // so we can allow mp regen to help manufacturer SystemMessage sm = new SystemMessage(SystemMessage.NOT_ENOUGH_MP); if (customer == null) manufacturer.sendPacket(sm); else customer.sendPacket(sm); manufacturer.isInCraftMode(false); activeMakers.remove(manufacturer); return; } manufacturer.reduceCurrentMp((double)recipeList.getMpCost()/(double)creationPasses); int numItems = skillLevel; while (numItems > 0 && !items.isEmpty()) // equip # of items we can in one run() { // if players complain they lose items during craft on server crashes, try L2ItemInstance item = items.get(0); // not to item.remove(), but instead copy it somehow and return if crafting int count = item.getCount(); // did not finish for some reason ?? umh don't know how it works really // when is sql db updated ? all the time ? sometimes ? if (count >= numItems) count = numItems; item.setCount(item.getCount() - count); if (item.getCount() <= 0) items.remove(0); numItems -= count; usedItems += count; SystemMessage sm = new SystemMessage(368); sm.addNumber(count); sm.addItemName(item.getItemId()); if (customer == null) manufacturer.sendPacket(sm); else customer.sendPacket(sm); } int delay = ((int)(manufacturer.getMReuseRate(manufacturer.getKnownSkill(172)) * GameTimeController.TICKS_PER_SECOND)) * GameTimeController.MILLIS_IN_TICK; MagicSkillUser msk = new MagicSkillUser(manufacturer, 1050, 1, delay, 0); manufacturer.broadcastPacket(msk); SetupGauge sg = new SetupGauge(0, delay); manufacturer.sendPacket(sg); StatusUpdate su = new StatusUpdate(manufacturer.getObjectId()); su.addAttribute(StatusUpdate.CUR_MP, (int) manufacturer.getCurrentMp()); manufacturer.sendPacket(su); // // this works but it makes craft of level1 items very fast for alt mode ... uhm // // I'd allow that, cause crafting "braided hemp" is not very fun and you need to do it // // in huge quantities, even if you are high level, so you get no XP/SP // // but it's not my call if (items.isEmpty()) { if(Config.DEBUG) _log.fine("saved "+delay+" miliseconds"); completeCreation(); manufacturer.isInCraftMode(false); activeMakers.remove(manufacturer); } else ClientScheduler.scheduleEvent(this, 1+delay); // Craft thread pool has nothing to do with other pools, so // only way this can make any kind of lag is when CPU usage is 100% // when nobody is "in craft mode" scheduled tasks for // craft thread pool must be 0 or we have a bug again :/ } private void completeCreation() // this is where real item production is done { int createdItemCount=0; if(Config.DEBUG) _log.fine(manufacturer.getName()+" : completeCreation() "); if(!Config.ALT_GAME_CREATION) // we reduce Mp in run() for ALT_GAME_CREATION manufacturer.reduceCurrentMp(recipeList.getMpCost()); StatusUpdate su = new StatusUpdate(manufacturer.getObjectId()); L2ItemInstance createdItem = ItemTable.getInstance().createItem(recipeList.getItemId()); createdItem.setCount(recipeList.getCount()); int randCheck = _rnd.nextInt(100); if(recipeList.getSuccessRate() <= randCheck ) { SystemMessage sm = new SystemMessage(614); sm.addString("Item(s) failed to create"); manufacturer.sendPacket(sm); manufacturer.isInCraftMode(false); if(Config.ALT_GAME_CREATION && activeMakers.get(manufacturer)!=null) activeMakers.remove(manufacturer); } else { // item succesfully created createdItemCount = createdItem.getCount(); SystemMessage sm = new SystemMessage(SystemMessage.EARNED_ITEM); sm.addItemName(createdItem.getItemId()); sm.addNumber(createdItemCount); if (customer == null) { manufacturer.getInventory().addItem(createdItem); manufacturer.sendPacket(sm); su.addAttribute(StatusUpdate.CUR_LOAD, manufacturer.getCurrentLoad()); } else { if (price > 0) { customer.reduceAdena(price); manufacturer.addAdena(price); L2ItemInstance adenaupdate = manufacturer.getInventory().findItemByItemId(57); InventoryUpdate iu = new InventoryUpdate(); iu.addModifiedItem(adenaupdate); manufacturer.sendPacket(iu); SystemMessage sm2 = new SystemMessage(SystemMessage.DISSAPEARED_ADENA); sm2.addNumber(price); customer.sendPacket(sm2); sm2 = new SystemMessage(SystemMessage.EARNED_ADENA); sm2.addNumber(price); manufacturer.sendPacket(sm2); } customer.getInventory().addItem(createdItem); customer.sendPacket(sm); StatusUpdate su2 = new StatusUpdate(customer.getObjectId()); su2.addAttribute(StatusUpdate.CUR_LOAD, customer.getCurrentLoad()); customer.sendPacket(su2); } } if (Config.ALT_GAME_CREATION) { int RecipeLevel = recipeList.getLevel(); int exp; int sp; exp = createdItem.getReferencePrice()*createdItemCount; // ensure at least some profit materialsRefPrice=(int)((double)materialsRefPrice*(double)0.95); // one variation if(Config.DEBUG) { _log.fine("BASED ON ITEM PRICE - MATERIALS PRICE"); _log.fine("createdItemCount: "+createdItemCount); _log.fine("exp: "+exp); _log.fine("referent price: "+createdItem.getReferencePrice()); _log.fine("referent total: "+createdItem.getReferencePrice()*createdItemCount); _log.fine("materials price: "+materialsRefPrice); _log.fine("exp: "+(exp-materialsRefPrice)); } exp -= materialsRefPrice; if (exp < 0) exp = 0; exp/=RecipeLevel; for (int i=skillLevel; i > RecipeLevel; i--) exp /= 4; sp = exp / 10; if(Config.DEBUG) _log.fine("exp with mods: "+exp); manufacturer.addExpAndSp((int)manufacturer.calcStat(Stats.EXPSP_RATE, exp * Config.RATE_XP, null, null), (int)manufacturer.calcStat(Stats.EXPSP_RATE, sp * Config.RATE_SP, null, null)); if(activeMakers.get(manufacturer)!=null) activeMakers.remove(manufacturer); } manufacturer.isInCraftMode(false); su.addAttribute(StatusUpdate.CUR_MP, (int) manufacturer.getCurrentMp()); manufacturer.sendPacket(su); if (customer == null){ manufacturer.sendPacket(new ItemList(manufacturer, false)); manufacturer.sendPacket(new RecipeItemMakeInfo(recipeList.getId(),manufacturer)); } else { customer.sendPacket(new ItemList(customer, false)); customer.sendPacket(new RecipeShopItemInfo(manufacturer.getObjectId(),recipeList.getId())); } } } }