RecipeController.java
-------------------------
/*
* 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 if it makes you happy
///
////
/////
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.L2PcInstance;
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.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<Integer, L2RecipeList> _lists;
protected static Map<L2PcInstance, RecipeItemMaker> activeMakers = Collections.synchronizedMap(new WeakHashMap<L2PcInstance,RecipeItemMaker>());
protected static final Random _rnd = new Random();
public static RecipeController getInstance()
{
if (_instance == null)
{
_instance = new RecipeController();
}
return _instance;
}
public RecipeController()
{
_lists = new FastMap<Integer, L2RecipeList>();
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)
{
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();
response.addRecipes(player.getRecipeBook());
player.sendPacket(response);
return;
}
if (Config.DEBUG) _log.fine("requestBookOpen: Busy creating");
SystemMessage sm = new SystemMessage(614);
sm.addString("You are busy creating ");
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<L2RecipeInstance> recipePartList = new FastList<L2RecipeInstance>();
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);
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<L2ItemInstance> items;
final 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<L2ItemInstance>();
this.skillLevel = manufacturer.getSkillLevel(L2Skill.SKILL_CRAFTING);
// 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(L2Skill.SKILL_CRAFTING)) * 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.scheduleCraftAction(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;
// if sitting, default formula, standing my new formula based on MP used to craft item(s)
if(!Config.ALT_GAME_CREATION_BY_MANA) // standard formula, tried to fix but no luck
{
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);
} else
{
exp=2*(recipeList.getLevel() * recipeList.getMpCost())
/skillLevel;
sp = exp / 10;
// one variation
if(Config.DEBUG)
{
_log.fine("NEW METHOD - BASED ON MANA");
_log.fine("createdItemCount: "+createdItemCount);
_log.fine("exp: "+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()));
}
}
}
}