/
com/planet_ink/coffee_mud/Abilities/Common/
com/planet_ink/coffee_mud/Abilities/Diseases/
com/planet_ink/coffee_mud/Abilities/Druid/
com/planet_ink/coffee_mud/Abilities/Fighter/
com/planet_ink/coffee_mud/Abilities/Languages/
com/planet_ink/coffee_mud/Abilities/Misc/
com/planet_ink/coffee_mud/Abilities/Prayers/
com/planet_ink/coffee_mud/Abilities/Properties/
com/planet_ink/coffee_mud/Abilities/Skills/
com/planet_ink/coffee_mud/Abilities/Songs/
com/planet_ink/coffee_mud/Abilities/Spells/
com/planet_ink/coffee_mud/Abilities/Thief/
com/planet_ink/coffee_mud/Abilities/Traps/
com/planet_ink/coffee_mud/Behaviors/
com/planet_ink/coffee_mud/CharClasses/interfaces/
com/planet_ink/coffee_mud/Commands/
com/planet_ink/coffee_mud/Commands/interfaces/
com/planet_ink/coffee_mud/Common/
com/planet_ink/coffee_mud/Common/interfaces/
com/planet_ink/coffee_mud/Exits/interfaces/
com/planet_ink/coffee_mud/Items/Armor/
com/planet_ink/coffee_mud/Items/Basic/
com/planet_ink/coffee_mud/Items/CompTech/
com/planet_ink/coffee_mud/Items/MiscMagic/
com/planet_ink/coffee_mud/Items/Weapons/
com/planet_ink/coffee_mud/Items/interfaces/
com/planet_ink/coffee_mud/Libraries/
com/planet_ink/coffee_mud/Libraries/interfaces/
com/planet_ink/coffee_mud/Locales/
com/planet_ink/coffee_mud/MOBS/
com/planet_ink/coffee_mud/Races/
com/planet_ink/coffee_mud/Races/interfaces/
com/planet_ink/coffee_mud/WebMacros/
com/planet_ink/coffee_mud/WebMacros/interfaces/
com/planet_ink/coffee_mud/core/
com/planet_ink/coffee_mud/core/collections/
com/planet_ink/coffee_mud/core/interfaces/
com/planet_ink/coffee_mud/core/intermud/
com/planet_ink/coffee_mud/core/intermud/i3/
com/planet_ink/coffee_web/server/
com/planet_ink/siplet/applet/
lib/
resources/factions/
resources/fakedb/
resources/progs/autoplayer/
resources/quests/holidays/
web/
web/admin.templates/
web/admin/grinder/
web/admin/images/
web/clan.templates/
web/pub.templates/
web/pub/images/mxp/
web/pub/sounds/
web/pub/textedit/
package com.planet_ink.coffee_mud.Libraries;
import com.planet_ink.coffee_mud.core.interfaces.*;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.collections.*;
import com.planet_ink.coffee_mud.core.exceptions.CMException;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.AbilityMapper.AbilityMapping;
import com.planet_ink.coffee_mud.Libraries.interfaces.AchievementLibrary.Achievement;
import com.planet_ink.coffee_mud.Libraries.interfaces.AchievementLibrary.Tracker;
import com.planet_ink.coffee_mud.Libraries.interfaces.ExpertiseLibrary.CostType;
import com.planet_ink.coffee_mud.Libraries.interfaces.ExpertiseLibrary.ExpertiseDefinition;
import com.planet_ink.coffee_mud.Libraries.interfaces.GenericEditor.CMEval;
import com.planet_ink.coffee_mud.Libraries.interfaces.MaskingLibrary.CompiledZapperMask;
import com.planet_ink.coffee_mud.Abilities.interfaces.*;
import com.planet_ink.coffee_mud.Areas.interfaces.*;
import com.planet_ink.coffee_mud.Behaviors.interfaces.*;
import com.planet_ink.coffee_mud.CharClasses.interfaces.*;
import com.planet_ink.coffee_mud.Commands.interfaces.*;
import com.planet_ink.coffee_mud.Common.interfaces.*;
import com.planet_ink.coffee_mud.Common.interfaces.AccountStats.Agent;
import com.planet_ink.coffee_mud.Exits.interfaces.*;
import com.planet_ink.coffee_mud.Items.interfaces.*;
import com.planet_ink.coffee_mud.Locales.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.interfaces.*;
import com.planet_ink.coffee_mud.Races.interfaces.*;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.*;

/*
   Copyright 2015-2016 Bo Zimmerman

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

	   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
public class Achievements extends StdLibrary implements AchievementLibrary
{
	@Override 
	public String ID()
	{
		return "Achievements";
	}
	
	// order is now significant, so they are Lists
	private List<Achievement> 			playerAchievements	= null;
	private List<Achievement> 			accountAchievements = null;
	private Map<Event,List<Achievement>>eventMap			= null;
	
	private final static String achievementFilename  = "achievements.ini";

	private static CMEval CMEVAL_INSTANCE = new CMEval()
	{
		@Override
		public Object eval(Object val, Object[] choices, boolean emptyOK) throws CMException
		{
			if(choices.length==0)
				return "";
			final String str=val.toString().trim();
			for(final Object o : choices)
			{
				if(str.equalsIgnoreCase(o.toString()))
					return o.toString();
			}
			throw new CMException("That was not one of your choices.");
		}
	};
	
	@Override
	public String evaluateAchievement(final Agent agent, String row, boolean addIfPossible)
	{
		if(row.trim().startsWith("#")||row.trim().startsWith(";")||(row.trim().length()==0))
			return null;
		int x=row.indexOf('=');
		while((x>=1)&&(row.charAt(x-1)=='\\'))
			x=row.indexOf('=',x+1);
		if(x<0)
			return "Error: Invalid line! Not comment, whitespace, and does not contain an = sign!";
		final String tattoo=row.substring(0,x).toUpperCase().trim();
		if(tattoo.length()==0)
			return "Error: Blank achievement tattoo: "+tattoo+"!";
		if(Character.isDigit(tattoo.charAt(0)))
			return "Error: Invalid achievement tattoo: "+tattoo+"!";
		final String params=row.substring(x+1).trim();
		String eventStr=CMParms.getParmStr(params, "EVENT", "");
		final Event eventType = (Event)CMath.s_valueOf(Event.class, eventStr.toUpperCase().trim());
		if(eventType == null)
			return "Error: Blank or unknown achievement type: "+eventStr+"!";
		final String displayStr=CMStrings.deEscape(CMParms.getParmStr(params, "DISPLAY", ""));
		final String titleStr=CMStrings.deEscape(CMParms.getParmStr(params, "TITLE", ""));
		String rewardStr=CMStrings.deEscape(CMParms.getParmStr(params, "REWARDS", ""));
		String[] awardSet = CMParms.parse(rewardStr).toArray(new String[0]);
		List<Award> awardsList = new ArrayList<Award>();
		if(titleStr.length()>0)
		{
			awardsList.add(new TitleAward()
			{
				@Override
				public AwardType getType()
				{
					return AwardType.TITLE;
				}

				@Override
				public String getTitle()
				{
					return titleStr;
				}
			});
		}
		for(int a=0;a<awardSet.length;a++)
		{
			if(awardSet[a].length()>0)
			{
				String thing = "";
				if(CMath.isInteger(awardSet[a]))
				{
					final int number = CMath.s_int(awardSet[a]);
					a++;
					while((a<awardSet.length)&&(awardSet[a].length()>0)&&(!CMath.isInteger(awardSet[a])))
					{
						thing += awardSet[a]+" ";
						a++;
					}
					a--;
					thing = thing.toUpperCase().trim();
					if(thing.equals("XP") || thing.startsWith("EXPERIEN") || thing.equals("EXP"))
					{
						awardsList.add(new AmountAward()
						{
							@Override
							public AwardType getType()
							{
								return AwardType.XP;
							}
							@Override
							public int getAmount()
							{
								return number;
							}
						});
					}
					else
					if(thing.equals("QP") || thing.startsWith("QUEST"))
					{
						awardsList.add(new AmountAward()
						{
							@Override
							public AwardType getType()
							{
								return AwardType.QP;
							}
							@Override
							public int getAmount()
							{
								return number;
							}
						});
					}
					else
					{
						int y=thing.indexOf('(');
						String parms="";
						if((y>0) && thing.endsWith(")"))
						{
							parms=thing.substring(y+1,thing.length()-1);
							thing=thing.substring(0,y);
						}
						Ability A=CMClass.getAbility(thing);
						if(A!=null)
						{
							String ableParms = CMStrings.deEscape(CMParms.getParmStr(parms, "PARMS", ""));
							String mask = CMStrings.deEscape(CMParms.getParmStr(parms, "MASK", ""));
							String preReqs = CMStrings.deEscape(CMParms.getParmStr(parms, "PREREQS", ""));
							boolean autoGain = CMParms.getParmBool(parms, "AUTOGAIN", true);
							final AbilityMapper.AbilityMapping mapp=CMLib.ableMapper().newAbilityMapping();
							mapp.abilityID(A.ID())
								.qualLevel(number)
								.autoGain(autoGain)
								.extraMask(mask)
								.defaultParm(ableParms);
							if(preReqs.length()>0)
							{
								mapp.originalSkillPreReqList(preReqs);
							}
							awardsList.add(new AbilityAward()
							{
								@Override
								public AwardType getType()
								{
									return AwardType.ABILITY;
								}

								@Override
								public AbilityMapping getAbilityMapping()
								{
									return mapp;
								}
							});
						}
						else
						if(CMLib.expertises().findDefinition(thing, true) != null)
						{
							final boolean isAutoGained = CMParms.getParmBool(parms, "AUTOGAIN", true);
							final ExpertiseDefinition oldDef=CMLib.expertises().findDefinition(thing, true);
							final String ID = oldDef.ID();
							final ExpertiseDefinition def = new ExpertiseDefinition()
							{
								volatile WeakReference<ExpertiseDefinition> ref=new WeakReference<ExpertiseDefinition>(CMLib.expertises().findDefinition(ID, true));
								private ExpertiseDefinition baseDef()
								{
									if(ref == null)
										return null;
									ExpertiseDefinition curDef = ref.get();
									if(curDef == null)
									{
										curDef = CMLib.expertises().findDefinition(ID, true); 
										if(curDef==null)
										{
											ref = null;
										}
										else
											ref = new WeakReference<ExpertiseDefinition>(curDef);
									}
									return curDef;
								}
								
								@Override
								public String ID()
								{
									return ID;
								}

								@Override
								public String name()
								{
									final ExpertiseDefinition curDef = baseDef();
									return (curDef == null) ? "" : curDef.name();
								}

								@Override
								public CMObject newInstance()
								{
									return baseDef();
								}

								@Override
								public CMObject copyOf()
								{
									return baseDef();
								}

								@Override
								public void initializeClass()
								{
								}

								@Override
								public int compareTo(CMObject o)
								{
									final ExpertiseDefinition curDef = baseDef();
									return (curDef == null) ? -1 : curDef.compareTo(o);
								}

								@Override
								public String getBaseName()
								{
									final ExpertiseDefinition curDef = baseDef();
									return (curDef == null) ? "" : curDef.getBaseName();
								}

								@Override
								public void setBaseName(String baseName)
								{
								}

								@Override
								public void setName(String name)
								{
								}

								@Override
								public void setID(String ID)
								{
								}

								@Override
								public void setData(String[] data)
								{
								}

								@Override
								public ExpertiseDefinition getParent()
								{
									final ExpertiseDefinition curDef = baseDef();
									return (curDef == null) ? null : curDef.getParent();
								}

								@Override
								public int getMinimumLevel()
								{
									return number;
								}

								@Override
								public String[] getData()
								{
									final ExpertiseDefinition curDef = baseDef();
									return (curDef == null) ? new String[0] : curDef.getData();
								}

								@Override
								public CompiledZapperMask compiledListMask()
								{
									return CMLib.masking().getPreCompiledMask("-LEVEL +>="+number);
								}

								@Override
								public CompiledZapperMask compiledFinalMask()
								{
									return CMLib.masking().createEmptyMask();
								}

								@Override
								public String allRequirements()
								{
									return "-LEVEL +>="+number;
								}

								@Override
								public String listRequirements()
								{
									return "-LEVEL +>="+number;
								}

								@Override
								public String finalRequirements()
								{
									return "";
								}

								@Override
								public void addListMask(String mask)
								{
								}

								@Override
								public void addFinalMask(String mask)
								{
								}

								@Override
								public void addCost(CostType type, Double value)
								{
								}

								@Override
								public String costDescription()
								{
									if(isAutoGained)
										return "";
									final ExpertiseDefinition curDef = baseDef();
									return (curDef == null) ? "" : curDef.costDescription();
								}

								@Override
								public boolean meetsCostRequirements(MOB mob)
								{
									if(isAutoGained)
										return true;
									final ExpertiseDefinition curDef = baseDef();
									return (curDef == null) ? false : curDef.meetsCostRequirements(mob);
								}

								@Override
								public void spendCostRequirements(MOB mob)
								{
									if(isAutoGained)
										return;
									final ExpertiseDefinition curDef = baseDef();
									if(curDef != null)
										curDef.spendCostRequirements(mob);
								}
							};
							awardsList.add(new ExpertiseAward()
							{
								@Override
								public AwardType getType()
								{
									return AwardType.EXPERTISE;
								}

								@Override
								public int getLevel()
								{
									return number;
								}

								@Override
								public ExpertiseDefinition getExpertise()
								{
									return def;
								}
							});
						}
						else
						if(CMLib.coffeeMaker().isAnyGenStat(CMClass.samplePlayer(), thing))
						{
							final String stat = thing.toUpperCase().trim();
							
							awardsList.add(new StatAward()
							{
								@Override
								public AwardType getType()
								{
									return AwardType.STAT;
								}

								@Override
								public int getAmount()
								{
									return number;
								}

								@Override
								public String getStat()
								{
									return stat;
								}
							});
						}
						else
						if(thing.toUpperCase().startsWith("ACCOUNT ")
						&&(CMClass.samplePlayer().playerStats()!=null)
						&&(CMClass.samplePlayer().playerStats().getAccount()!=null)
						&&CMLib.coffeeMaker().isAnyGenStat(CMClass.samplePlayer(), thing.substring(8)))
						{
							final String stat = thing.toUpperCase().trim();
							
							awardsList.add(new StatAward()
							{
								@Override
								public AwardType getType()
								{
									return AwardType.STAT;
								}

								@Override
								public int getAmount()
								{
									return number;
								}

								@Override
								public String getStat()
								{
									return stat;
								}
							});
						}
						else
						{
							String currency = CMLib.english().matchAnyCurrencySet(thing);
							if(currency == null)
								Log.debugOut("Achievement", "Unknown award type: "+thing);
							else
							{
								double denomination = CMLib.english().matchAnyDenomination(currency, thing);
								if(denomination == 0.0)
									Log.debugOut("Achievement", "Unknown award type: "+thing);
								else
								{
									final String currencyName = thing;
									awardsList.add(new CurrencyAward()
									{
										@Override
										public AwardType getType()
										{
											return AwardType.CURRENCY;
										}
										@Override
										public int getAmount()
										{
											return number;
										}
										@Override
										public String getCurrency()
										{
											return currencyName;
										}
									});
								}
							}
						}
					}
				}
			}
		}
		final Award[] rewardList = awardsList.toArray(new Award[0]); 
		Achievement A;
		switch(eventType)
		{
		case KILLS:
			A=new Achievement()
			{
				private int num = 0;
				private MaskingLibrary.CompiledZapperMask npcMask = null;
				private MaskingLibrary.CompiledZapperMask playerMask = null;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}
				
				@Override
				public Agent getAgent()
				{
					return agent;
				}
				
				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return num;
					
				}
				
				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}
				
				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile int count = oldCount;

						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return (getCount(mob) >= num);
						}

						@Override
						public int getCount(MOB mob)
						{
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms)
						{
							if((parms.length>0)
							&&(parms[0] instanceof MOB)
							&&((npcMask==null)||(CMLib.masking().maskCheck(npcMask, (MOB)parms[0], true)))
							&&((playerMask==null)||(CMLib.masking().maskCheck(playerMask, mob, true))))
							{
								count+=bumpNum;
								return true;
							}
							return false;
						}
						
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return true;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "NUM", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid NUM parameter: "+numStr+"!";
					num=CMath.s_int(numStr);
					String zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "ZAPPERMASK", ""));
					if(zapperMask.trim().length()==0)
						return "Error: Missing or invalid ZAPPERMASK parameter: "+zapperMask+"!";
					this.npcMask = CMLib.masking().getPreCompiledMask(zapperMask);
					zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "PLAYERMASK", ""));
					this.playerMask = null;
					if(zapperMask.trim().length()>0)
						this.playerMask = CMLib.masking().getPreCompiledMask(zapperMask);
					return "";
				}
			};
			break;
		case JUSTBE:
			A=new Achievement()
			{
				private MaskingLibrary.CompiledZapperMask playerMask = null;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}
				
				@Override
				public Agent getAgent()
				{
					return agent;
				}
				
				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return Integer.MIN_VALUE;
				}
				
				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}
				
				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							if((playerMask==null)||(CMLib.masking().maskCheck(playerMask, mob, true)))
								return true;
							return false;
						}

						@Override
						public int getCount(MOB mob)
						{
							return 0;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms)
						{
							if((parms.length>0)
							&&(parms[0] instanceof MOB)
							&&((playerMask==null)||(CMLib.masking().maskCheck(playerMask, mob, true))))
							{
								return true;
							}
							return false;
						}
						
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return false;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					String zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "PLAYERMASK", ""));
					if(zapperMask.trim().length()==0)
						return "Error: Missing or invalid PLAYERMASK parameter: "+zapperMask+"!";
					this.playerMask = CMLib.masking().getPreCompiledMask(zapperMask);
					return "";
				}
			};
			break;
		case STATVALUE:
			A=new Achievement()
			{
				private String	statName= "";
				private int 	value	= 0;
				private int		abelo	= 0;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}
				
				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return value;
				}

				@Override
				public boolean isTargetFloor()
				{
					return abelo > 0;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return (abelo > 0) ? (getCount(mob) > value) : (getCount(mob) < value);
						}

						@Override
						public int getCount(MOB mob)
						{
							return CMath.s_int(CMLib.coffeeMaker().getAnyGenStat(mob, statName));
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return false;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					MOB mob = CMClass.getFactoryMOB();
					final String numStr=CMParms.getParmStr(parms, "VALUE", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid VALUE parameter: "+numStr+"!";
					value=CMath.s_int(numStr);
					final String aboveBelow=CMParms.getParmStr(parms, "ABOVEBELOW", "").toUpperCase().trim();
					if((!aboveBelow.equals("ABOVE")) && (!aboveBelow.equals("BELOW")))
						return "Error: Missing or invalid ABOVEBELOW parameter: "+aboveBelow+"!";
					this.abelo = aboveBelow.equals("ABOVE")? 1 : -1;
					final String statName=CMParms.getParmStr(parms, "STAT", "").toUpperCase().trim();
					if((statName.length()==0)||(!CMLib.coffeeMaker().isAnyGenStat(mob, statName)))
						return "Error: Missing or invalid STAT parameter: "+statName+"!";
					this.statName = statName;
					return "";
				}
			};
			break;
		case FACTION:
			A=new Achievement()
			{
				private String	factionID	= "";
				private int 	value		= 0;
				private int		abelo		= 0;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public boolean isTargetFloor()
				{
					return abelo > 0;
				}
				
				@Override
				public int getTargetCount()
				{
					return value;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return (abelo > 0) ? (getCount(mob) > value) : (getCount(mob) < value);
						}

						@Override
						public int getCount(MOB mob)
						{
							return mob.fetchFaction(factionID);
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return false;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "VALUE", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid VALUE parameter: "+numStr+"!";
					value=CMath.s_int(numStr);
					final String aboveBelow=CMParms.getParmStr(parms, "ABOVEBELOW", "").toUpperCase().trim();
					if((!aboveBelow.equals("ABOVE")) && (!aboveBelow.equals("BELOW")))
						return "Error: Missing or invalid ABOVEBELOW parameter: "+aboveBelow+"!";
					this.abelo = aboveBelow.equals("ABOVE")? 1 : -1;
					final String factionID=CMParms.getParmStr(parms, "ID", "").toUpperCase().trim();
					if(factionID.length()==0)
						return "Error: Missing ID parameter: "+factionID+"!";
					if(CMLib.factions().getFaction(factionID)==null)
						return "Error: Unknown faction ID parameter: "+factionID+"!";
					this.factionID=factionID;
					return "";
				}
			};
			break;
		case EXPLORE:
			A=new Achievement()
			{
				private String	areaID	= "";
				private int	 	pct		= 0;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public int getTargetCount()
				{
					return pct;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public boolean isTargetFloor()
				{
					return true;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}
				
				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return getCount(mob) >= pct;
						}

						@Override
						public int getCount(MOB mob)
						{
							final PlayerStats pstats=mob.playerStats();
							if(pstats != null)
							{
								if(areaID.equals("WORLD"))
								{
									final Room R=mob.location();
									if((R!=null)&&(CMLib.map().getExtendedRoomID(CMLib.map().getRoom(R)).length()>0))
										return pstats.percentVisited(mob,null);
									else
										return 0;
								}
								else
								{
									final Area A=CMLib.map().getArea(areaID);
									if(A!=null)
									{
										return pstats.percentVisited(mob, A);
									}
								}
							}
							return 0;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return false;
				}

				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "PERCENT", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid PERCENT parameter: "+numStr+"!";
					this.pct=CMath.s_int(numStr);
					final String areaID=CMParms.getParmStr(parms, "AREA", "").toUpperCase().trim();
					if(areaID.length()==0)
						return "Error: Missing AREA parameter: "+areaID+"!";
					if((CMLib.map().getArea(areaID)==null)&&(!areaID.equals("WORLD")))
						return "Error: Unknown AREA: "+areaID+"!";
					this.areaID=areaID;
					return "";
				}
			};
			break;
		case CRAFTING:
		case MENDER:
			A=new Achievement()
			{
				private int 				num 		= 0;
				private final Set<String>	abilityIDs 	= new TreeSet<String>();
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return num;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public boolean isTargetFloor()
				{
					return true;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}
				
				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile int count = oldCount;
						
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return getCount(mob) >= num;
						}

						@Override
						public int getCount(MOB mob)
						{
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							final Ability A;
							if(parms.length>0)
							{
								if(parms[0] instanceof String)
									A=CMClass.getAbility((String)parms[0]);
								else
								if(parms[0] instanceof Ability)
									A=(Ability)parms[0];
								else
									A=null;
								if((A!=null)&&(abilityIDs.contains("*")||abilityIDs.contains(A.ID())))
								{
									count+=bumpNum;
									return true;
								}
							}
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return true;
				}

				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "NUM", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid NUM parameter: "+numStr+"!";
					this.num=CMath.s_int(numStr);
					final String abilityIDs=CMParms.getParmStr(parms, "ABILITYID", "").toUpperCase().trim();
					if(abilityIDs.length()==0)
						return "Error: Missing ABILITYID parameter: "+abilityIDs+"!";
					final String[] strList=abilityIDs.split(",");
					this.abilityIDs.clear();
					for(int i=0;i<strList.length;i++)
					{
						String abilityID = strList[i].trim();
						if(abilityID.equals("*"))
						{
							this.abilityIDs.add(abilityID);
							break;
						}
						else
						{
							final Ability A=CMClass.getAbility(abilityID);
							if((A==null)
							||((CMClass.getAbility(abilityID).classificationCode() & Ability.ALL_ACODES)!=Ability.ACODE_COMMON_SKILL)
							||((CMClass.getAbility(abilityID).classificationCode() & Ability.ALL_DOMAINS)!=Ability.DOMAIN_CRAFTINGSKILL))
								return "Error: Unknown crafting ABILITYID: "+abilityID+"!";
							this.abilityIDs.add(A.ID());
						}
					}
					if(this.abilityIDs.size()==0)
						return "Error: Unknown crafting ABILITYIDs: "+abilityIDs+"!";
					return "";
				}
			};
			break;
		case SKILLUSE:
			A=new Achievement()
			{
				private int 				num 		= 0;
				private final Set<String>	abilityIDs 	= new TreeSet<String>();
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public int getTargetCount()
				{
					return num;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile int count = oldCount;
						
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return getCount(mob) >= num;
						}

						@Override
						public int getCount(MOB mob)
						{
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							final Ability A;
							if(parms.length>0)
							{
								if(parms[0] instanceof String)
									A=CMClass.getAbility((String)parms[0]);
								else
								if(parms[0] instanceof Ability)
									A=(Ability)parms[0];
								else
									A=null;
								if((A!=null)
								&&(abilityIDs.contains("*")
									||abilityIDs.contains(A.ID())
									||(abilityIDs.contains(Ability.ACODE_DESCS[A.classificationCode()&Ability.ALL_ACODES]))
									||(abilityIDs.contains(Ability.DOMAIN_DESCS[(A.classificationCode()&Ability.ALL_DOMAINS)>>5]))))
								{
									count+=bumpNum;
									return true;
								}
							}
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return true;
				}

				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "NUM", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid NUM parameter: "+numStr+"!";
					this.num=CMath.s_int(numStr);
					final String abilityIDs=CMParms.getParmStr(parms, "ABILITYID", "").toUpperCase().trim();
					if(abilityIDs.length()==0)
						return "Error: Missing ABILITYID parameter: "+abilityIDs+"!";
					final String[] strList=abilityIDs.split(",");
					this.abilityIDs.clear();
					for(int i=0;i<strList.length;i++)
					{
						String abilityID = strList[i].trim();
						if(abilityID.equals("*"))
						{
							this.abilityIDs.add(abilityID);
							break;
						}
						else
						{
							final Ability A;
							if(CMParms.contains(Ability.ACODE_DESCS,abilityID)
							||CMParms.contains(Ability.DOMAIN_DESCS,abilityID))
								A=null;
							else
							{
								A=CMClass.getAbility(abilityID);
								if(A==null)
									return "Error: Unknown ABILITYID: "+abilityID+"!";
							}
							if(A!=null)
								this.abilityIDs.add(A.ID());
							else
								this.abilityIDs.add(abilityID.toUpperCase());
						}
					}
					if(this.abilityIDs.size()==0)
						return "Error: Unknown crafting ABILITYIDs: "+abilityIDs+"!";
					return "";
				}
			};
			break;
		case QUESTOR:
			A=new Achievement()
			{
				private int num = 0;
				private MaskingLibrary.CompiledZapperMask mask = null;
				private java.util.regex.Pattern questPattern = null;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public int getTargetCount()
				{
					return num;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile int count = oldCount;
						
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return getCount(mob) >= num;
						}

						@Override
						public int getCount(MOB mob)
						{
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							if((mask!=null)&&(!CMLib.masking().maskCheck(mask, mob, true)))
								return false;
							if(parms.length>0)
							{
								boolean match=questPattern == null;
								if(!match)
								{
									if(parms[0] instanceof String)
									{
										match = questPattern.matcher((String)parms[0]).find();
									}
									else
									if(parms[0] instanceof Quest)
									{
										Quest Q=(Quest)parms[0];
										match = 
											questPattern.matcher(Q.name()).find()
											|| questPattern.matcher(Q.displayName()).find();
									}
								}
								if(match)
								{
									count+=bumpNum;
									return true;
								}
							}
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return true;
				}

				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "NUM", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid NUM parameter: "+numStr+"!";
					num=CMath.s_int(numStr);
					final String zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "PLAYERMASK", ""));
					if(zapperMask.trim().length()>0)
						this.mask = CMLib.masking().getPreCompiledMask(zapperMask);
					else
						this.mask = null;
					final String questMask=CMStrings.deEscape(CMParms.getParmStr(parms, "QUESTMASK", ""));
					this.questPattern = null;
					if(questMask.trim().length()>0)
					{
						try
						{
							java.util.regex.Pattern P = java.util.regex.Pattern.compile(questMask);
							if(P!=null)
								questPattern = P;
						}
						catch(Exception e)
						{
							return "Error: Invalid QUESTMASK regular expression parameter: "+questMask+": "+e.getMessage()+"!";
						}
					}
					return "";
				}
			};
			break;
		case ACHIEVER:
			A=new Achievement()
			{
				final Set<String> achievementList = new TreeSet<String>();
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public int getTargetCount()
				{
					if(achievementList.size()<2)
						return Integer.MIN_VALUE;
					else
						return achievementList.size();
				}

				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return getCount(mob) >= achievementList.size();
						}

						@Override
						public int getCount(MOB mob)
						{
							int count = 0;
							Tattooable other;
							if((CMProps.isUsingAccountSystem())
							&&(mob.playerStats()!=null))
								other=mob.playerStats().getAccount();
							else
								other=null;
							for(String s : achievementList)
							{
								if(mob.findTattoo(s)!=null)
									count++;
								else
								if((other!=null)&&(other.findTattoo(s)!=null))
									count++;
							}
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return false;
				}

				@Override
				public String parseParms(final String parms)
				{
					final String list=CMStrings.deEscape(CMParms.getParmStr(parms, "ACHIEVEMENTLIST", ""));
					if(list.trim().length()==0)
						return "Error: Missing or invalid ACHIEVEMENTLIST parameter: "+list+"!";
					final String[] listArray = list.toUpperCase().trim().split(",");
					achievementList.clear();
					for(String s : listArray)
					{
						if(s.trim().length()>0)
							achievementList.add(s.trim());
					}
					if(achievementList.size()==0)
						return "Error: Missing or invalid ACHIEVEMENTLIST parameter: "+list+"!";
					return "";
				}
			};
			break;
		case ROOMENTER:
			A=new Achievement()
			{
				final Set<String> roomIDs = new TreeSet<String>();
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public int getTargetCount()
				{
					return roomIDs.size();
				}

				@Override
				public boolean isTargetFloor()
				{
					return true;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return getCount(mob) >= roomIDs.size();
						}

						@Override
						public int getCount(MOB mob)
						{
							int count = 0;
							for(String s : roomIDs)
							{
								if(mob.playerStats().hasVisited(CMLib.map().getRoom(s)))
									count++;
							}
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms) 
						{
							return false;
						}
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return false;
				}

				@Override
				public String parseParms(final String parms)
				{
					final String list=CMStrings.deEscape(CMParms.getParmStr(parms, "ROOMID", ""));
					if(list.trim().length()==0)
						return "Error: Missing or invalid ROOMID parameter: "+list+"!";
					final String[] listArray = list.toUpperCase().trim().split(",");
					roomIDs.clear();
					for(String s : listArray)
					{
						s=s.trim();
						if(s.length()>0)
						{
							Room R=CMLib.map().getRoom(s);
							if(R==null)
							{
								if(CMProps.getBoolVar(CMProps.Bool.MUDSTARTED))
									return "Error: Missing or invalid ROOMID: "+s+"!";
								else
									roomIDs.add(s);
							}
							else
								roomIDs.add(CMLib.map().getExtendedRoomID(R));
						}
					}
					if(roomIDs.size()==0)
						return "Error: Missing or invalid ROOMID parameter: "+list+"!";
					return "";
				}
			};
			break;
		case RETIRE:
		case REMORT:
		case LEVELSGAINED:
			A=new Achievement()
			{
				private int num = 0;
				private MaskingLibrary.CompiledZapperMask playerMask = null;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}

				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return num;
				}

				@Override
				public boolean isTargetFloor()
				{
					return true;
				}

				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile int count = oldCount;

						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return (getCount(mob) >= num);
						}

						@Override
						public int getCount(MOB mob)
						{
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms)
						{
							if((playerMask==null)||(CMLib.masking().maskCheck(playerMask, mob, true)))
							{
								count+=bumpNum;
								return true;
							}
							return false;
						}
						
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return true;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "NUM", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid NUM parameter: "+numStr+"!";
					num=CMath.s_int(numStr);
					String zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "PLAYERMASK", ""));
					if(zapperMask.trim().length()>0)
						this.playerMask = CMLib.masking().getPreCompiledMask(zapperMask);
					return "";
				}
			};
			break;
		case GOTITEM:
			A=new Achievement()
			{
				private MaskingLibrary.CompiledZapperMask playerMask = null;
				private MaskingLibrary.CompiledZapperMask itemMask = null;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}
				
				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return Integer.MIN_VALUE;
				}
				
				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile boolean gotIt=false;

						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return gotIt;
						}

						@Override
						public int getCount(MOB mob)
						{
							return 0;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms)
						{
							if((playerMask==null)||(CMLib.masking().maskCheck(playerMask, mob, true)))
							{
								if((parms[0] instanceof Item)
								&&((itemMask==null)||(CMLib.masking().maskCheck(itemMask, (Item)parms[0], true))))
								{
									gotIt = true;
									return true;
								}
							}
							return false;
						}
						
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return false;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					String itemZapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "ITEMMASK", ""));
					if(itemZapperMask.trim().length()>0)
						this.itemMask = CMLib.masking().getPreCompiledMask(itemZapperMask);
					else
						return "Error: Missing or invalid ITEMMASK parameter!";
					String zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "PLAYERMASK", ""));
					if(zapperMask.trim().length()>0)
						this.playerMask = CMLib.masking().getPreCompiledMask(zapperMask);
					return "";
				}
			};
			break;
		case TIMEPLAYED:
			A=new Achievement()
			{
				private int seconds = 0;
				private MaskingLibrary.CompiledZapperMask playerMask = null;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}

				@Override
				public Agent getAgent()
				{
					return agent;
				}
				
				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return seconds;
					
				}
				
				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}

				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile int count = oldCount;

						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return (getCount(mob) >= seconds);
						}

						@Override
						public int getCount(MOB mob)
						{
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms)
						{
							if((playerMask==null)||(CMLib.masking().maskCheck(playerMask, mob, true)))
							{
								count+=bumpNum;
								return true;
							}
							return false;
						}
						
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return true;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "SECONDS", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid SECONDS parameter: "+numStr+"!";
					seconds=CMath.s_int(numStr);
					String zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "PLAYERMASK", ""));
					if(zapperMask.trim().length()>0)
						this.playerMask = CMLib.masking().getPreCompiledMask(zapperMask);
					return "";
				}
			};
			break;
		case DEATHS:
			A=new Achievement()
			{
				private int num = 0;
				private MaskingLibrary.CompiledZapperMask npcMask = null;
				private MaskingLibrary.CompiledZapperMask playerMask = null;
				
				@Override
				public Event getEvent()
				{
					return eventType;
				}
				
				@Override
				public Agent getAgent()
				{
					return agent;
				}
				
				@Override
				public String getTattoo()
				{
					return tattoo;
				}

				@Override
				public int getTargetCount()
				{
					return num;
					
				}
				
				@Override
				public boolean isTargetFloor()
				{
					return true;
				}
				
				@Override
				public String getDisplayStr()
				{
					return displayStr;
				}

				@Override
				public Award[] getRewards()
				{
					return rewardList;
				}
				
				@Override
				public String getRawParmVal(String str)
				{
					return CMParms.getParmStr(params,str,"");
				}

				@Override
				public Tracker getTracker(final int oldCount)
				{
					final Achievement me=this;
					return new Tracker()
					{
						private volatile int count = oldCount;

						@Override
						public Achievement getAchievement() 
						{
							return me;
						}

						@Override
						public boolean isAchieved(MOB mob) 
						{
							return (getCount(mob) >= num);
						}

						@Override
						public int getCount(MOB mob)
						{
							return count;
						}

						@Override
						public boolean testBump(MOB mob, int bumpNum, Object... parms)
						{
							if((parms.length>0)
							&&(parms[0] instanceof MOB)
							&&((npcMask==null)||(CMLib.masking().maskCheck(npcMask, (MOB)parms[0], true)))
							&&((playerMask==null)||(CMLib.masking().maskCheck(playerMask, mob, true))))
							{
								count+=bumpNum;
								return true;
							}
							return false;
						}
						
					};
				}
				
				@Override
				public boolean isSavableTracker()
				{
					return true;
				}
				
				@Override
				public String parseParms(final String parms)
				{
					final String numStr=CMParms.getParmStr(parms, "NUM", "");
					if(!CMath.isInteger(numStr))
						return "Error: Missing or invalid NUM parameter: "+numStr+"!";
					num=CMath.s_int(numStr);
					this.npcMask = null;
					String zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "ZAPPERMASK", ""));
					if(zapperMask.trim().length()>=0)
						this.npcMask = CMLib.masking().getPreCompiledMask(zapperMask);
					zapperMask=CMStrings.deEscape(CMParms.getParmStr(parms, "PLAYERMASK", ""));
					this.playerMask = null;
					if(zapperMask.trim().length()>0)
						this.playerMask = CMLib.masking().getPreCompiledMask(zapperMask);
					return "";
				}
			};
			break;
			default:
				A=null;
				break;
		}
		
		if(A==null)
		{
			return "Error: Unimplemented achievement type: "+eventStr+"!";
		}
		
		final String err = A.parseParms(params);
		if((err!=null)&&(err.length()>0))
		{
			return err;
		}
		
		if(addIfPossible)
		{
			// the call to achievements below will ensure others are loaded.
			for(final Enumeration<Achievement> a2  = achievements(null); a2.hasMoreElements();)
			{
				final Achievement A2=a2.nextElement();
				if(A2.getTattoo().equalsIgnoreCase(tattoo))
					return "Error: Duplicate achievement ID: "+tattoo+"!";
			}
			switch(agent)
			{
			case ACCOUNT:
				accountAchievements.add(A);
				break;
			case PLAYER:
				playerAchievements.add(A);
				break;
			}
			List<Achievement> eventList = eventMap.get(A.getEvent());
			if(eventList == null)
			{
				eventList = new LinkedList<Achievement>();
				eventMap.put(A.getEvent(), eventList);
			}
			eventList.add(A);
		}
		return null;
	}
	
	private void ensureAchievementsLoaded()
	{
		if((playerAchievements==null)||(accountAchievements==null))
		{
			synchronized(this)
			{
				if((playerAchievements==null)||(accountAchievements==null))
				{
					reloadAchievements();
				}
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public synchronized Enumeration<Achievement> achievements(Agent agent)
	{
		ensureAchievementsLoaded();
		if(agent == null)
		{
			return new MultiEnumeration<Achievement>(new Enumeration[]{
				new IteratorEnumeration<Achievement>(accountAchievements.iterator()),
				new IteratorEnumeration<Achievement>(playerAchievements.iterator()),
			});
		}
		switch(agent)
		{
		case ACCOUNT:
			return new IteratorEnumeration<Achievement>(accountAchievements.iterator());
		default:
		case PLAYER:
			return new IteratorEnumeration<Achievement>(playerAchievements.iterator());
		}
	}

	protected void possiblyBumpPlayerAchievement(final MOB mob, final Achievement A, final PlayerStats pStats, final Event E, int bumpNum, Object... parms)
	{
		if(mob.findTattoo(A.getTattoo())==null)
		{
			final Tracker T=pStats.getAchievementTracker(A, mob);
			if(T.testBump(mob, bumpNum, parms))
			{
				if(T.isAchieved(mob))
				{
					giveAwards(A,mob,mob);
				}
			}
		}
	}
	
	protected void possiblyBumpAccountAchievement(final MOB mob, final Achievement A, final PlayerAccount account, final Event E, int bumpNum, Object... parms)
	{
		if(account != null)
		{
			if(account.findTattoo(A.getTattoo())==null)
			{
				final Tracker T=account.getAchievementTracker(A, mob);
				if(T.testBump(mob, bumpNum, parms))
				{
					if(T.isAchieved(mob))
					{
						giveAwards(A,account,mob);
					}
				}
			}
		}
	}
	
	@Override
	public void possiblyBumpAchievement(final MOB mob, final Event E, int bumpNum, Object... parms)
	{
		if((mob != null)&&(E!=null)&&(!mob.isMonster())&&(CMProps.getBoolVar(CMProps.Bool.MUDSTARTED)))
		{
			ensureAchievementsLoaded();
			final PlayerStats pStats = mob.playerStats();
			if(pStats != null)
			{
				if(eventMap.containsKey(E))
				{
					final PlayerAccount account = pStats.getAccount();
					for(final Achievement A :  eventMap.get(E))
					{
						switch(A.getAgent())
						{
						case PLAYER:
							possiblyBumpPlayerAchievement(mob, A, pStats, E, bumpNum, parms);
							break;
						case ACCOUNT:
							possiblyBumpAccountAchievement(mob, A, account, E, bumpNum, parms);
							break;
						}
					}
				}
			}
		}
	}

	public void giveAwards(final MOB mob, final Award[] awardSet)
	{
		if(mob == null)
			return;
		final PlayerStats pStats = mob.playerStats();
		StringBuilder awardMessage = new StringBuilder("");
		for(final Award award : awardSet)
		{
			switch(award.getType())
			{
			case ABILITY:
			{
				final AbilityAward aaward = (AbilityAward)award;
				if((pStats!=null) && (!pStats.getExtraQualifiedSkills().containsKey(aaward.getAbilityMapping().abilityID())))
				{
					final Ability A=CMClass.getAbility(aaward.getAbilityMapping().abilityID());
					if(A!=null)
					{
						pStats.getExtraQualifiedSkills().put(A.ID(), aaward.getAbilityMapping());
						awardMessage.append(L("^HYou are awarded a qualification for @x1 at level @x2!\n\r^?",A.name(),""+aaward.getAbilityMapping().qualLevel()));
					}
				}
				break;
			}
			case CURRENCY:
			{
				final CurrencyAward aaward=(CurrencyAward)award;
				String currency = CMLib.english().matchAnyCurrencySet(aaward.getCurrency());
				if(currency != null)
				{
					double denomination = CMLib.english().matchAnyDenomination(currency, aaward.getCurrency());
					if(denomination != 0.0)
					{
						double money=CMath.mul(aaward.getAmount(),  denomination);
						CMLib.beanCounter().giveSomeoneMoney(mob, money);
						awardMessage.append(L("^HYou are awarded @x1!\n\r^?",CMLib.beanCounter().getDenominationName(currency, denomination, aaward.getAmount())));
					}
				}
				break;
			}
			case STAT:
			{
				final StatAward aaward=(StatAward)award;
				if((mob.playerStats()!=null)
				&&(mob.playerStats().getAccount()!=null)
				&&(aaward.getStat().startsWith("ACCOUNT ")))
				{
					final String stat = aaward.getStat().substring(8).trim();
					String value = mob.playerStats().getAccount().getStat(stat);
					if(CMath.isNumber(value))
					{
						awardMessage.append(L("^HYour account is awarded @x1!\n\r^?",aaward.getAmount() + " " + stat));
						mob.playerStats().getAccount().setStat(stat, "" + (CMath.s_int(value) + aaward.getAmount()));
					}
				}
				else
				{
					String value = CMLib.coffeeMaker().getAnyGenStat(mob, aaward.getStat());
					if(CMath.isNumber(value))
					{
						awardMessage.append(L("^HYou are awarded @x1!\n\r^?",aaward.getAmount() + " " + aaward.getStat()));
						CMLib.coffeeMaker().setAnyGenStat(mob, aaward.getStat(), "" + (CMath.s_int(value) + aaward.getAmount()));
					}
				}
				break;
			}
			case EXPERTISE:
			{
				final ExpertiseAward aaward = (ExpertiseAward)award;
				if(pStats!=null)
				{
					if(!pStats.getExtraQualifiedExpertises().containsKey(aaward.getExpertise().ID()))
					{
						pStats.getExtraQualifiedExpertises().put(aaward.getExpertise().ID(), aaward.getExpertise());
						awardMessage.append(L("^HYou are awarded a qualification for @x1 at level @x2!\n\r^?",aaward.getExpertise().name(),""+aaward.getLevel()));
					}
				}
				break;
			}
			case QP:
			{
				final AmountAward aaward=(AmountAward)award;
				awardMessage.append(L("^HYou are awarded @x1 quest points!\n\r^?\n\r",""+aaward.getAmount()));
				mob.setQuestPoint(mob.getQuestPoint() + aaward.getAmount());
				break;
			}
			case TITLE:
			{
				final TitleAward aaward=(TitleAward)award;
				if((pStats != null) && (!pStats.getTitles().contains(aaward.getTitle())))
				{
					pStats.getTitles().add(aaward.getTitle());
					awardMessage.append(L("^HYou are awarded the title: @x1!\n\r^?",CMStrings.replaceAll(aaward.getTitle(),"*",mob.Name())));
				}
				break;
			}
			case XP:
			{
				final AmountAward aaward=(AmountAward)award;
				awardMessage.append(L("^HYou are awarded experience points!\n\r^?\n\r"));
				CMLib.leveler().postExperience(mob, null, null, aaward.getAmount(), false);
				break;
			}
			default:
				break;
			
			}
		}
		mob.tell(awardMessage.toString());
		grantAbilitiesAndExpertises(mob);
	}
	
	private boolean giveAwards(final Achievement A, final Tattooable holder, final MOB mob)
	{
		if(holder.findTattoo(A.getTattoo())==null)
		{
			holder.addTattoo(A.getTattoo());
			StringBuilder awardMessage = new StringBuilder(L("^HYou have completed the '@x1' @x2 achievement!^?\n\r",A.getDisplayStr(),A.getAgent().name().toLowerCase()));
			final List<String> channels=CMLib.channels().getFlaggedChannelNames(ChannelsLibrary.ChannelFlag.ACHIEVEMENTS);
			if(!CMLib.flags().isCloaked(mob))
			{
				final PlayerStats pStats = mob.playerStats();
				final PlayerAccount account = (pStats != null) ? pStats.getAccount() : null;
				final String name = ((A.getAgent() == Agent.ACCOUNT) && (account != null)) ? account.getAccountName() : mob.name();
				for(int i=0;i<channels.size();i++)
					CMLib.commands().postChannel(channels.get(i),mob.clans(),L("@x1 has completed the '@x2' @x3 achievement!",name,A.getDisplayStr(),A.getAgent().name().toLowerCase()),true);
			}
			final Award[] awardSet = A.getRewards();
			mob.tell(awardMessage.toString());
			if(A.getAgent() == Agent.PLAYER)
			{
				giveAwards(mob,awardSet);
			}
			return true;
		}
		return false;
	}
	
	@Override
	public Achievement getAchievement(String tattoo)
	{
		for(Enumeration<Achievement> a = achievements(null); a.hasMoreElements();)
		{
			final Achievement A=a.nextElement();
			if(A.getTattoo().equalsIgnoreCase(tattoo))
				return A;
		}
		return null;
	}
	
	@Override
	public Achievement deleteAchievement(String tattoo)
	{
		final Achievement A=getAchievement(tattoo);
		if(A!=null)
		{
			switch(A.getAgent())
			{
			case ACCOUNT:
				accountAchievements.remove(A);
				break;
			case PLAYER:
				playerAchievements.remove(A);
				break;
			}
			final List<Achievement> list=eventMap.get(A.getEvent());
			if(list != null)
				list.remove(A);
		}
		return A;
	}
	
	@Override
	public String getAchievementsHelpFromMap(Map<String,Map<String,String>> helpMap, Event E, String parmName)
	{
		Map<String,String> entryMap;
		if(E==null)
			entryMap = helpMap.get("");
		else
			entryMap = helpMap.get(E.name());
		if((entryMap == null) && (E!=null))
			entryMap = helpMap.get("");
		if(entryMap == null)
		{
			for(Map<String,String> map : helpMap.values())
			{
				if(map.containsKey(parmName))
				{
					entryMap = map;
					break;
				}
			}
		}
		if(entryMap == null)
			return null;
		if(entryMap.containsKey(parmName))
			return entryMap.get(parmName);
		for(Map<String,String> map : helpMap.values())
		{
			if(map.containsKey(parmName))
				return map.get(parmName);
		}
		return null;
	}
	
	@Override
	public Map<String,Map<String,String>> getAchievementsHelpMap()
	{
		final Map<String,Map<String,String>> help = new TreeMap<String,Map<String,String>>();
		
		final String achievementFilename = getAchievementFilename();
		final List<String> V=Resources.getFileLineVector(Resources.getRawFileResource(achievementFilename,true));
		Resources.removeResource(achievementFilename);
		String eventName = "";
		String keyName = "";
		for(String s : V)
		{
			s=s.trim();
			if((s.length()==0)
			||((!s.startsWith("#"))&&(!s.startsWith(";"))))
				continue;
			s=s.substring(1);
			int x=s.indexOf("EVENT=\"");
			if(x>=0)
			{
				int y=s.indexOf("\"",x+7);
				if(y>x)
					eventName=s.substring(x+7,y);
				if(CMath.s_valueOf(Event.class, eventName)==null)
					eventName="";
				continue;
			}
			Map<String,String> parmMap=help.get(eventName);
			if(parmMap==null)
			{
				parmMap=new TreeMap<String,String>();
				help.put(eventName, parmMap);
			}
			String value;
			if(s.trim().startsWith("["))
			{
				int start=s.indexOf('[');
				x=s.indexOf(']',start+1);
				if(x>0)
				{
					keyName = s.substring(start+1,x);
					value = s.substring(x+1); 
				}
				else
					value="";
			}
			else
				value = s;
			if((keyName.length()>0)&&(value.length()>0))
			{
				String oldS=parmMap.containsKey(keyName) ? parmMap.get(keyName) : "";
				value = oldS + "\n\r" + value;
				parmMap.put(keyName, value);
			}
			
		}
		return help;
	}

	public String buildRow(final Event E, Map<String,String> parmTree)
	{
		StringBuilder str=new StringBuilder(parmTree.get("TATTOO")+"=");
		for(final String parmName : E.getParameters())
		{
			String value = parmTree.get(parmName);
			if((value != null) && (value.trim().length()>0))
			{
				str.append(parmName+"=");
				if(CMath.isMathExpression(value))
					str.append(value).append(" ");
				else
					str.append("\"").append(CMStrings.escape(value)).append("\" ");
			}
		}
		return str.toString();
	}

	@Override
	public String getAwardString(final Award[] awards)
	{
		final StringBuilder awardStr=new StringBuilder();
		for(Award award : awards)
		{
			switch(award.getType())
			{
			case ABILITY:
			{
				final AbilityMapping map = ((AbilityAward)award).getAbilityMapping();
				awardStr.append(" ").append(map.qualLevel());
				final StringBuilder parms=new StringBuilder("");
				if(!map.autoGain())
					parms.append(" AUTOGAIN=FALSE");
				if((map.extraMask().length()>0)&&(map.extraMask().length()>0))
					parms.append(" MASK="+CMStrings.escape("\""+CMStrings.escape(map.extraMask()))+"\"");
				if((map.defaultParm().length()>0)&&(map.defaultParm().length()>0))
					parms.append(" PARMS="+CMStrings.escape("\""+CMStrings.escape(map.defaultParm()))+"\"");
				if((map.originalSkillPreReqList()!=null)&&(map.originalSkillPreReqList().length()>0))
					parms.append(" PREREQS="+CMStrings.escape("\""+CMStrings.escape(map.originalSkillPreReqList()))+"\"");
				if(parms.toString().trim().length()>0)
				{
					awardStr.append(" \"")
							.append(map.abilityID())
							.append("(").append(parms).append(")")
							.append("\"");
				}
				else
					awardStr.append(" ").append(map.abilityID());
				break;
			}
			case CURRENCY:
				awardStr.append(" ").append(((CurrencyAward)award).getAmount())
						.append(" ").append(((CurrencyAward)award).getCurrency());
				break;
			case STAT:
				awardStr.append(" ").append(((StatAward)award).getAmount())
						.append(" ").append(((StatAward)award).getStat());
				break;
			case EXPERTISE:
				awardStr.append(" ").append(((ExpertiseAward)award).getLevel())
						.append(" ").append(((ExpertiseAward)award).getExpertise().ID());
				break;
			case QP:
				awardStr.append(" ").append(((AmountAward)award).getAmount())
						.append(" ").append("QP");
				break;
			case TITLE:
				break;
			case XP:
				awardStr.append(" ").append(((AmountAward)award).getAmount())
						.append(" ").append("XP");
				break;
			default:
				break;
			}
		}
		return awardStr.toString();
	}
	
	private void fillAchievementParmTree(final Map<String,String> parmTree, final Achievement A)
	{
		parmTree.put("TATTOO",A.getTattoo());
		parmTree.put("EVENT", A.getEvent().name());
		parmTree.put("DISPLAY", A.getDisplayStr());
		parmTree.put("TITLE", "");
		for(Award award : A.getRewards())
		{
			if(award.getType() == AwardType.TITLE)
				parmTree.put("TITLE", ((TitleAward)award).getTitle());
		}
		String awardStr=getAwardString(A.getRewards());
		parmTree.put("REWARDS", awardStr);
		for(String s : A.getEvent().getParameters())
		{
			if(!CMParms.contains(AchievementLibrary.BASE_ACHIEVEMENT_PARAMETERS, s))
				parmTree.put(s, CMStrings.deEscape(A.getRawParmVal(s)));
		}
	}
	
	@Override
	public boolean addModifyAchievement(final MOB mob, final Agent agent, final String tattoo, Achievement A)
	{
		if(mob.isMonster())
			return false;
		boolean ok=false;
		int showFlag=-1;
		if(CMProps.getIntVar(CMProps.Int.EDITORTYPE)>0)
			showFlag=-999;
		final TreeMap<String,String> parmTree=new TreeMap<String,String>();
		for(final Event E : Event.values())
		{
			for(String s : E.getParameters())
				parmTree.put(s,"");
		}
		final Map<String,Map<String,String>> helpMap = getAchievementsHelpMap();
		Event E=Event.KILLS;
		parmTree.put("TATTOO",tattoo.toUpperCase().trim());
		if(A!=null)
		{
			E=A.getEvent();
			fillAchievementParmTree(parmTree,A);
		}
		parmTree.put("EVENT",E.name());
		try 
		{
			while((mob.session()!=null)&&(!mob.session().isStopped())&&(!ok))
			{
				int showNumber=0;
				String help;
				
				help=getAchievementsHelpFromMap(helpMap,null,"EVENT");
				parmTree.put("EVENT",CMLib.genEd().prompt(mob, parmTree.get("EVENT"), ++showNumber, showFlag, L("Event Type"), false, false, help, CMEVAL_INSTANCE, Event.getEventChoices()));
				E = (Event)CMath.s_valueOf(Event.class, parmTree.get("EVENT"));
				
				help=getAchievementsHelpFromMap(helpMap,null,"DISPLAY");
				parmTree.put("DISPLAY",CMLib.genEd().prompt(mob, parmTree.get("DISPLAY"), ++showNumber, showFlag, L("Display Desc"), false, false, help, null, null));
				
				help=getAchievementsHelpFromMap(helpMap,null,"TITLE");
				parmTree.put("TITLE",CMLib.genEd().prompt(mob, parmTree.get("TITLE"), ++showNumber, showFlag, L("Title Award"), true, false, help, null, null));
				
				help=getAchievementsHelpFromMap(helpMap,null,"REWARDS");
				parmTree.put("REWARDS",CMLib.genEd().prompt(mob, parmTree.get("REWARDS"), ++showNumber, showFlag, L("Rewards List"), true, false, help, null, null));
				
				for(final String parmName : E.getParameters())
				{
					if(!CMStrings.contains(BASE_ACHIEVEMENT_PARAMETERS, parmName))
					{
						help=getAchievementsHelpFromMap(helpMap,E,parmName);
						parmTree.put(parmName,CMLib.genEd().prompt(mob, parmTree.get(parmName), ++showNumber, showFlag, CMStrings.capitalizeAndLower(parmName), false, false, help, null, null));
					}
				}
				
				for(final String parmName : parmTree.keySet())
				{
					if((!parmName.equals("TATTOO"))&&(!CMStrings.contains(E.getParameters(), parmName)))
						parmTree.put(parmName, "");
				}
				
				String achievementRow = buildRow(E,parmTree);
				String err = evaluateAchievement(agent, achievementRow, false);
				if((err != null)&&(err.trim().length()>0)&&(mob.session()!=null))
				{
					mob.session().println(L("^HERRORS: ^r@x1^N",err));
				}
				else
				if(showFlag<-900)
				{ 
					ok=true; 
					break;
				}
				if(showFlag>0)
				{ 
					showFlag=-1; 
					continue;
				}
				final String promptStr=mob.session().prompt(L("Edit which (or CANCEL)? "),"");
				showFlag=CMath.s_int(promptStr);
				if(showFlag<=0)
				{
					if(promptStr.equalsIgnoreCase("CANCEL"))
					{
						mob.tell(L("Canceled."));
						return false;
					}
					else
					if((err != null)&&(err.trim().length()>0)&&(mob.session()!=null))
					{
						mob.session().println(L("^HCorrect errors first or enter CANCEL: ^r@x1^N",err));
						showFlag=-1;
						continue;
					}
					else
					{
						showFlag=-1;
						ok=true;
					}
				}
			}
			if(A!=null)
			{
				this.deleteAchievement(A.getTattoo());
			}
			this.evaluateAchievement(agent, buildRow(E,parmTree), true);
			for(Enumeration<MOB> m = CMLib.players().players();m.hasMoreElements();)
			{
				final MOB M=m.nextElement();
				if(M.playerStats()!=null)
				{
					M.playerStats().rebuildAchievementTracker(M, parmTree.get("TATTOO"));
				}
			}
			this.resaveAchievements(parmTree.get("TATTOO"));
			return true;
		} 
		catch (IOException e) 
		{
			return false;
		}
	}

	private void finishSaveAchievementsSection(final Agent agent, final Set<String> added, final String EOL, final StringBuffer newFileData)
	{
		for(final Enumeration<Achievement> a=achievements(agent);a.hasMoreElements();)
		{
			final Achievement A=a.nextElement();
			if(!added.contains(A.getTattoo().toUpperCase().trim()))
			{
				final Map<String,String> parmTree = new TreeMap<String,String>();
				fillAchievementParmTree(parmTree,A);
				newFileData.append(buildRow(A.getEvent(),parmTree)).append(EOL);
			}
		}
		added.clear(); // reset for the next section
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void resaveAchievements(final String modifyTattoo)
	{
		final String loadAchievementFilename = getAchievementFilename();
		final StringBuffer buf = Resources.getRawFileResource(loadAchievementFilename,true);
		Resources.removeResource(loadAchievementFilename);
		final List<String> V=Resources.getFileLineVector(buf);
		final StringBuffer newFileData = new StringBuffer("");
		final Map<String,Achievement>[] maps=new Map[Agent.values().length];
		for(int i=0;i<Agent.values().length;i++)
			maps[i]=new TreeMap<String,Achievement>();
		for(Agent agent : Agent.values())
		{
			for(final Enumeration<Achievement> a=achievements(agent);a.hasMoreElements();)
			{
				final Achievement A=a.nextElement();
				if(A.getAgent() == agent)
					maps[agent.ordinal()].put(A.getTattoo().toUpperCase().trim(), A);
			}
		}
		final String EOL= Resources.getEOLineMarker(buf);
		final Set<String> added = new HashSet<String>();
		Agent currentAgent = Agent.PLAYER;
		for(String row : V)
		{
			if(row.trim().startsWith("#")||row.trim().startsWith(";")||(row.trim().length()==0))
				newFileData.append(row).append(EOL);
			else
			{
				int x=row.indexOf('=');
				if(x<=0)
				{
					if(row.trim().startsWith("["))
					{
						Agent oldAgent = currentAgent;
						for(Agent ag : Agent.values())
						{
							if(row.trim().toUpperCase().equals("["+ag.name()+"]"))
							{
								currentAgent = ag;
								break;
							}
						}
						if(oldAgent != currentAgent)
						{
							finishSaveAchievementsSection(oldAgent, added, EOL, newFileData);
						}
					}
					newFileData.append(row).append(EOL);
				}
				else
				{
					final String tatt = row.substring(0,x).trim();
					if(maps[currentAgent.ordinal()].containsKey(tatt.toUpperCase().trim()))
					{
						final Achievement A=maps[currentAgent.ordinal()].get(tatt);
						if((modifyTattoo != null)&&(modifyTattoo.equalsIgnoreCase(tatt)))
						{
							final Map<String,String> parmTree = new TreeMap<String,String>();
							fillAchievementParmTree(parmTree,A);
							newFileData.append(buildRow(A.getEvent(),parmTree)).append(EOL);
						}
						else
							newFileData.append(row).append(EOL);
						added.add(tatt.toUpperCase().trim());
					}
				}
			}
		}
		finishSaveAchievementsSection(currentAgent, added, EOL, newFileData);
		Resources.updateFileResource(achievementFilename, newFileData);
		Resources.removeResource(achievementFilename);
	}
	
	private boolean evaluatePlayerAchievement(final Achievement A, final PlayerStats pStats, final MOB mob)
	{
		if(mob.findTattoo(A.getTattoo())==null)
		{
			final Tracker T=pStats.getAchievementTracker(A, mob);
			if(T.isAchieved(mob))
			{
				return giveAwards(A, mob, mob);
			}
		}
		return false;
	}
	
	private boolean evaluateAccountAchievement(final Achievement A, final PlayerAccount account, final MOB mob)
	{
		if(account != null)
		{
			if(account.findTattoo(A.getTattoo())==null)
			{
				final Tracker T=account.getAchievementTracker(A, mob);
				if(T.isAchieved(mob))
				{
					return giveAwards(A, account, mob);
				}
			}
		}
		return false;
	}
	
	
	private boolean evaluateAchievement(final Achievement A, final PlayerStats pStats, final PlayerAccount aStats, final MOB mob)
	{
		switch(A.getAgent())
		{
		case PLAYER:
			return evaluatePlayerAchievement(A,pStats,mob);
		case ACCOUNT:
			return evaluateAccountAchievement(A,aStats,mob);
		}
		return false;
	}
	
	@Override
	public boolean evaluateAchievements(final MOB mob)
	{
		if((mob==null)||(!CMProps.getBoolVar(CMProps.Bool.MUDSTARTED)))
			return false;
		final PlayerStats P=mob.playerStats();
		if(P==null)
			return false;
		final PlayerAccount C=P.getAccount() != null ? P.getAccount() : null;
		
		boolean somethingDone = false;
		for(Enumeration<Achievement> a=achievements(null);a.hasMoreElements();)
		{
			final Achievement A=a.nextElement();
			if(evaluateAchievement(A,P,C,mob))
				somethingDone = true;
		}
		return somethingDone;
	}
	
	private String getAchievementFilename()
	{
		CMFile F = new CMFile(Resources.makeFileResourceName(achievementFilename),null);
		if(F.exists() && (F.canRead()))
			return achievementFilename;
		final String oldFilename = achievementFilename.substring(0,achievementFilename.length()-4)+".txt"; 
		F = new CMFile(Resources.makeFileResourceName(oldFilename),null);
		if(F.exists() && (F.canRead()))
			return oldFilename;
		return achievementFilename;
	}

	@Override
	public synchronized void reloadAchievements()
	{
		accountAchievements=new SLinkedList<Achievement>();
		playerAchievements=new SLinkedList<Achievement>();
		eventMap=new TreeMap<Event,List<Achievement>>();
		final String achievementFilename = getAchievementFilename();
		final List<String> V=Resources.getFileLineVector(Resources.getRawFileResource(achievementFilename,true));
		Resources.removeResource(achievementFilename);
		String WKID=null;
		Agent agent = Agent.PLAYER;
		for(int v=0;v<V.size();v++)
		{
			final String row=V.get(v);
			if(row.trim().startsWith("["))
			{
				final String upTrimRow = row.trim().toUpperCase();
				boolean found=false;
				for(Agent ag : Agent.values())
				{
					if(upTrimRow.equals("["+ag.name()+"]"))
					{
						agent=ag;
						found=true;
						break;
					}
				}
				if(!found)
					Log.errOut("Achievements","Unknown section name in "+achievementFilename+": "+row);
				continue;
			}
			WKID=evaluateAchievement(agent,row,true);
			if(WKID==null)
				continue;
			if(WKID.startsWith("Error: "))
				Log.errOut("Achievements",WKID);
		}
		for(final Enumeration<MOB> e=CMLib.players().players();e.hasMoreElements();)
		{
			final MOB M=e.nextElement();
			if(M.playerStats()!=null)
			{
				if((evaluateAchievements(M))&&(!CMLib.flags().isInTheGame(M,true)))
					CMLib.database().DBUpdatePlayerPlayerStats(M);
			}
		}
	}

	@Override
	public void grantAbilitiesAndExpertises(MOB mob)
	{
		final PlayerStats pStats = (mob == null) ? null : mob.playerStats();
		if((pStats != null) && (mob!=null))
		{
			for(final AbilityMapper.AbilityMapping map : pStats.getExtraQualifiedSkills().values())
			{
				if(map.autoGain() && (mob.fetchAbility(map.abilityID()) == null))
				{
					final Ability A=CMClass.getAbility(map.abilityID());
					if((A!=null)
					&&(map.qualLevel()<=mob.basePhyStats().level())
					&&((map.extraMask().length()==0)||(CMLib.masking().maskCheck(map.extraMask(),mob,true))))
					{
						A.setSavable(true);
						A.setProficiency(map.defaultProficiency());
						A.setMiscText(map.defaultParm());
						mob.addAbility(A);
						A.autoInvocation(mob, false);
					}
				}
			}
			for(final ExpertiseDefinition def : pStats.getExtraQualifiedExpertises().values())
			{
				if((def.costDescription().length()==0)
				&&((def.compiledListMask()==null)||CMLib.masking().maskCheck(def.compiledListMask(), mob, true))
				&&((def.compiledFinalMask()==null)||CMLib.masking().maskCheck(def.compiledFinalMask(), mob, true))
				&&(mob.fetchExpertise(def.ID())==null))
				{
					mob.addExpertise(def.ID());
				}
			}
		}
	}
	
	@Override
	public void loadPlayerSkillAwards(Tattooable mob, PlayerStats stats)
	{
		if((mob != null) && (stats != null))
		{
			for(final Enumeration<Tattoo> t = mob.tattoos();t.hasMoreElements();)
			{
				final Achievement A=getAchievement(t.nextElement().getTattooName());
				if(A != null)
				{
					for(final Award award : A.getRewards())
					{
						if(award.getType() == AwardType.ABILITY)
						{
							final AbilityAward aaward = (AbilityAward)award;
							if(!stats.getExtraQualifiedSkills().containsKey(aaward.getAbilityMapping().abilityID()))
							{
								final Ability abilityCheck=CMClass.getAbility(aaward.getAbilityMapping().abilityID());
								if(abilityCheck!=null)
									stats.getExtraQualifiedSkills().put(abilityCheck.ID(), aaward.getAbilityMapping());
							}
						}
						else
						if(award.getType() == AwardType.EXPERTISE)
						{
							final ExpertiseAward aaward = (ExpertiseAward)award;
							if(!stats.getExtraQualifiedExpertises().containsKey(aaward.getExpertise().ID()))
								stats.getExtraQualifiedExpertises().put(aaward.getExpertise().ID(), aaward.getExpertise());
						}
					}
				}
			}
		}
	}

	@Override
	public void loadAccountAchievements(final MOB mob)
	{
		final PlayerStats pStats = (mob==null) ? null : mob.playerStats();
		final PlayerAccount account = (pStats == null) ? null : pStats.getAccount();
		if((mob!=null) && (account != null))
		{
			boolean somethingDone = false;
			for(Enumeration<Tattoo> t=account.tattoos();t.hasMoreElements();)
			{
				final Tattoo T = t.nextElement();
				final Achievement A=getAchievement(T.getTattooName());
				if(A != null)
				{
					if(mob.findTattoo(T.getTattooName())==null)
					{
						mob.addTattoo(A.getTattoo());
						giveAwards(mob, A.getRewards());
						somethingDone=true;
					}
				}
			}
			if(somethingDone)
			{
				loadPlayerSkillAwards(mob, pStats);
				grantAbilitiesAndExpertises(mob);
			}
		}
	}
	
	protected String makeAchievmentHelp(Achievement A)
	{
		if(A == null)
			return null;
		@SuppressWarnings("unchecked")
		Map<String,String> helpMap = (Map<String,String>)Resources.getResource("SYSTEM_ACHIEVEMENT_HELP");
		if(helpMap == null)
		{
			helpMap = new Hashtable<String,String>();
			Resources.submitResource("SYSTEM_ACHIEVEMENT_HELP", helpMap);
		}
		String help = helpMap.get(A.getTattoo());
		if(help != null)
			return help;
		StringBuilder str = new StringBuilder("");
		int cols = 20;
		if(A.getAgent() == Agent.ACCOUNT)
			str.append("\n\r").append(CMStrings.padRight(L("Account Achievement:"),cols)).append(" ").append(A.getTattoo());
		else
			str.append("\n\r").append(CMStrings.padRight(L("Char. Achievement:"),cols)).append(" ").append(A.getTattoo());
		str.append("\n\r").append(CMStrings.padRight(L("Description:"),cols)).append(" ").append(A.getDisplayStr());
		str.append("\n\r").append(CMStrings.padRight(L("Achievement Type:"),cols)).append(" ");
		str.append(L(A.getEvent().displayName()));
		if(A.getRewards().length>0)
		{
			if(A.getAgent() == Agent.ACCOUNT)
				str.append("\n\r").append(CMStrings.padRight(L("Rewards Granted:"),cols)).append(" ").append(L("New Characters, Children, and Remorted"));
			else
				str.append("\n\r").append(CMStrings.padRight(L("Rewards Granted:"),cols)).append(" ").append(L("Immediately"));
		}
		for(Award W : A.getRewards())
		{
			switch(W.getType())
			{
			case ABILITY:
			{
				AbilityAward award = (AbilityAward)W;
				Ability skillA = CMClass.getAbility(award.getAbilityMapping().abilityID());
				str.append("\n\r").append(CMStrings.padRight(L("Award:"),cols)).append(" ").append(skillA.name());
				if(award.getAbilityMapping().autoGain())
					str.append(L(" at level @x1",""+award.getAbilityMapping().qualLevel()));
				else
					str.append(L(" qualification level @x1",""+award.getAbilityMapping().qualLevel()));
				break;
			}
			case CURRENCY:
			{
				CurrencyAward award = (CurrencyAward)W;
				str.append("\n\r").append(CMStrings.padRight(L("Award:"),cols)).append(" ")
					.append(award.getAmount()).append(" ").append(CMStrings.capitalizeAndLower(award.getCurrency()));
				break;
			}
			case EXPERTISE:
			{
				ExpertiseAward award = (ExpertiseAward)W;
				ExpertiseDefinition defE = award.getExpertise();
				str.append("\n\r").append(CMStrings.padRight(L("Award:"),cols)).append(" ").append(defE.name());
				if(defE.costDescription().length()==0)
					str.append(L(" at level @x1",""+defE.getMinimumLevel()));
				else
					str.append(L(" qualification level @x1",""+defE.getMinimumLevel()));
				break;
			}
			case QP:
			{
				AmountAward award = (AmountAward)W;
				str.append("\n\r").append(CMStrings.padRight(L("Award:"),cols)).append(" ")
					.append(award.getAmount()).append(" ").append(L("Quest Points"));
				break;
			}
			case STAT:
			{
				StatAward award = (StatAward)W;
				str.append("\n\r").append(CMStrings.padRight(L("Award:"),cols)).append(" +")
					.append(award.getAmount()).append(" ").append(L(CMStrings.capitalizeAndLower(award.getStat())));
				break;
			}
			case TITLE:
			{
				TitleAward T = (TitleAward)W;
				str.append("\n\r").append(CMStrings.padRight(L("Award:"),cols)).append(" ")
				.append(L("The title: ")).append(T.getTitle());
				break;
			}
			case XP:
			{
				AmountAward award = (AmountAward)W;
				str.append("\n\r").append(CMStrings.padRight(L("Award:"),cols)).append(" ")
					.append(award.getAmount()).append(" ").append(L("Experience Points"));
				break;
			}
			}
		}
		str.append("\n\r");
		helpMap.put(A.getTattoo(), str.toString());
		return str.toString();
	}
	
	@Override
	public String getAchievementsHelp(String ID, boolean exact)
	{
		if(ID==null)
			return null;
		ID = ID.replace('`','\'');
		Achievement A=this.getAchievement(ID.toUpperCase());
		if(A == null)
		{
			for(final Enumeration<Achievement> a=achievements(null); a.hasMoreElements();)
			{
				final Achievement A2 = a.nextElement();
				if(A2.getDisplayStr().equalsIgnoreCase(ID))
				{
					A=A2;
					break;
				}
			}
		}
		if(exact)
			return makeAchievmentHelp(A);
		if(A == null)
		{
			for(final Enumeration<Achievement> a=achievements(null); a.hasMoreElements();)
			{
				final Achievement A2 = a.nextElement();
				if(A2.getTattoo().toUpperCase().startsWith(ID.toUpperCase()))
				{
					A=A2;
					break;
				}
			}
		}
		if(A == null)
		{
			for(final Enumeration<Achievement> a=achievements(null); a.hasMoreElements();)
			{
				final Achievement A2 = a.nextElement();
				if(A2.getDisplayStr().toLowerCase().startsWith(ID.toLowerCase()))
				{
					A=A2;
					break;
				}
			}
		}
		return makeAchievmentHelp(A);
	}
	
	@Override
	public boolean shutdown()
	{
		Resources.removeResource("SYSTEM_ACHIEVEMENT_HELP");
		accountAchievements=null;
		playerAchievements=null;
		eventMap=null;
		return super.shutdown();
	}
}