@Override
				public String convertFromItem(final ItemCraftor A, final Item I)
				{
					if(!(I instanceof DeadBody)) 
						return "";
					String pose=I.displayText();
					pose=CMStrings.replaceAll(pose,I.name(),"<S-NAME>");
					pose=CMStrings.replaceWord(pose,"himself"," <S-HIM-HERSELF>");
					pose=CMStrings.replaceWord(pose,"herself"," <S-HIM-HERSELF>");
					pose=CMStrings.replaceWord(pose,"his"," <S-HIS-HER>");
					pose=CMStrings.replaceWord(pose,"her"," <S-HIS-HER>");
					pose=CMStrings.replaceWord(pose,"him"," <S-HIM-HER>");
					pose=CMStrings.replaceWord(pose,"her"," <S-HIM-HER>");
					return pose;
				}
			},
			new AbilityParmEditorImpl("WOOD_METAL_CLOTH","",ParmType.CHOICES)
			{
				@Override
				public void createChoices()
				{
					createChoices(new String[] { "WOOD", "METAL", "CLOTH" });
				}
	
				@Override
				public String defaultValue()
				{
					return "WOOD";
				}
	
				@Override
				public String convertFromItem(final ItemCraftor A, final Item I)
				{
					switch(I.material()&RawMaterial.MATERIAL_MASK)
					{
					case RawMaterial.MATERIAL_CLOTH: return "CLOTH";
					case RawMaterial.MATERIAL_METAL: return "METAL";
					case RawMaterial.MATERIAL_MITHRIL: return "METAL";
					case RawMaterial.MATERIAL_WOODEN: return "WOOD";
					default: return "";
					}
				}
			},
			new AbilityParmEditorImpl("WEAPON_TYPE","W.Type",ParmType.CHOICES)
			{
				@Override
				public int appliesToClass(Object o)
				{
					return (o instanceof Weapon) ? 2 : -1;
				}
	
				@Override
				public void createChoices()
				{
					createChoices(Weapon.TYPE_DESCS);
				}
	
				@Override
				public String convertFromItem(final ItemCraftor A, final Item I)
				{
					return (I instanceof Weapon) ? Weapon.TYPE_DESCS[((Weapon) I).weaponDamageType()] : "";
				}
	
				@Override
				public String defaultValue()
				{
					return "BASHING";
				}
			},
			new AbilityParmEditorImpl("ATTACK_MODIFICATION","Att.",ParmType.NUMBER)
			{
				@Override
				public void createChoices()
				{
				}
	
				@Override
				public int appliesToClass(Object o)
				{
					return (o instanceof Weapon) ? 2 : -1;
				}
	
				@Override
				public String convertFromItem(final ItemCraftor A, final Item I)
				{
					return "" + ((I instanceof Weapon) ? ((Weapon) I).basePhyStats().attackAdjustment() : 0);
				}
	
				@Override
				public String defaultValue()
				{
					return "0";
				}
			},
			new AbilityParmEditorImpl("N_A","N/A",ParmType.STRING)
			{
				@Override
				public void createChoices()
				{
				}
	
				@Override
				public int appliesToClass(Object o)
				{
					return -1;
				}
	
				@Override
				public String defaultValue()
				{
					return "";
				}
	
				@Override
				public String convertFromItem(final ItemCraftor A, final Item I)
				{
					return "";
				}
	
				@Override
				public boolean confirmValue(String oldVal)
				{
					return oldVal.trim().length() == 0 || oldVal.equals("0") || oldVal.equals("NA") || oldVal.equals("-");
				}
	
				@Override
				public String commandLinePrompt(MOB mob, String oldVal, int[] showNumber, int showFlag) throws java.io.IOException
				{
					return "";
				}
	
				@Override
				public String webField(HTTPRequest httpReq, java.util.Map<String, String> parms, String oldVal, String fieldName)
				{
					return "";
				}
			},
			new AbilityParmEditorImpl("RESOURCE_NAME_AMOUNT_MATERIAL_REQUIRED","Resrc/Amt",ParmType.SPECIAL)
			{
				@Override
				public void createChoices() 
				{
					createChoices(RawMaterial.CODES.NAMES());
					choices().add("","");
				}

				@Override
				public String convertFromItem(final ItemCraftor A, final Item I)
				{
					int amt=(int)Math.round(CMath.mul(I.basePhyStats().weight()-1,(A!=null)?A.getItemWeightMultiplier(false):1.0));
					if(amt<1)
						amt=1;
					return RawMaterial.CODES.NAME(I.material())+"/"+amt;
				}
	
				@Override
				public String defaultValue()
				{
					return "";
				}
	
				@Override
				public int appliesToClass(Object o)
				{
					return 0;
				}
	
				@Override
				public String webValue(HTTPRequest httpReq, java.util.Map<String,String> parms, String oldVal, String fieldName)
				{
					if(httpReq.isUrlParameter(fieldName+"_RESOURCE"))
					{
						final String rsc=httpReq.getUrlParameter(fieldName+"_RESOURCE");
						final String amt=httpReq.getUrlParameter(fieldName+"_AMOUNT");
						if((rsc.trim().length()==0)||(rsc.equalsIgnoreCase("NOTHING"))||(CMath.s_int(amt)<=0))
							return "";
						return rsc+"/"+amt;
					}
					return oldVal;
				}

				@Override
				public String webField(HTTPRequest httpReq, java.util.Map<String,String> parms, String oldVal, String fieldName)
				{
					final String value=webValue(httpReq,parms,oldVal,fieldName);
					String rsc = "";
					int amt = 0;
					final int x=value.indexOf('/');
					if(x>0)
					{
						rsc = value.substring(0,x);
						amt = CMath.s_int(value.substring(x+1));
					}
					final StringBuffer str=new StringBuffer("");
					str.append("\n\r<SELECT NAME="+fieldName+"_RESOURCE MULTIPLE>");
					final String[] Ss=RawMaterial.CODES.NAMES().clone();
					Arrays.sort(Ss);
					for(final String S : Ss)
					{
						str.append("<OPTION VALUE=\""+S+"\" "
								+((S.equalsIgnoreCase(rsc))?"SELECTED":"")+">"
								+CMStrings.capitalizeAndLower(S));
					}
					str.append("</SELECT>");
					str.append("&nbsp;&nbsp;Amount: ");
					str.append("<INPUT TYPE=TEXT NAME="+fieldName+"_AMOUNT VALUE="+amt+">");
					return str.toString();
				}

				@Override
				public boolean confirmValue(String oldVal) 
				{
					if(oldVal.trim().length()==0)
						return true;
					oldVal=oldVal.trim();
					final int x=oldVal.indexOf('/');
					if(x<0)
						return false;
					if(!CMStrings.contains(choices().toArrayFirst(new String[0]),oldVal.substring(0,x)))
						return false;
					if(!CMath.isInteger(oldVal.substring(x+1)))
						return false;
					return true;
				}

				@Override
				public String[] fakeUserInput(String oldVal) 
				{
					final int x=oldVal.indexOf('/');
					if(x<=0) return new String[]{""};
					return new String[]{oldVal.substring(0,x),oldVal.substring(x+1)};
				}

				@Override
				public String commandLinePrompt(MOB mob, String oldVal, int[] showNumber, int showFlag) throws java.io.IOException
				{
					oldVal=oldVal.trim();
					final int x=oldVal.indexOf('/');
					String oldRsc = "";
					int oldAmt = 0;
					if(x>0)
					{
						oldRsc = oldVal.substring(0,x);
						oldAmt = CMath.s_int(oldVal.substring(x));
					}
					oldRsc = CMLib.genEd().prompt(mob,oldRsc,++showNumber[0],showFlag,prompt(),choices());
					if(oldRsc.length()>0)
						return oldRsc+"/"+CMLib.genEd().prompt(mob,oldAmt,++showNumber[0],showFlag,prompt());
					return "";
				}
			},
		});
		DEFAULT_EDITORS = new Hashtable<String,AbilityParmEditor>();
		for(int v=0;v<V.size();v++)
		{
			final AbilityParmEditor A = V.elementAt(v);
			DEFAULT_EDITORS.put(A.ID(),A);
		}
		return DEFAULT_EDITORS;
	}

	protected class AbilityRecipeDataImpl implements AbilityRecipeData
	{
		private String recipeFilename;
		private String recipeFormat;
		private Vector<Object> columns;
		private Vector<DVector> dataRows;
		private int numberOfDataColumns;
		public String[] columnHeaders;
		public int[] columnLengths;
		public int classFieldIndex;
		private String parseError = null;
		private boolean wasVFS = false;

		public AbilityRecipeDataImpl(String recipeFilename, String recipeFormat)
		{
			this.recipeFilename = recipeFilename;
			this.recipeFormat = recipeFormat;
			if(recipeFilename.trim().length()==0)
			{
				parseError = "No file";
				return;
			}
			final CMFile F = new CMFile(Resources.buildResourcePath("skills")+recipeFilename,null,CMFile.FLAG_LOGERRORS);
			wasVFS=F.isVFSFile();
			final StringBuffer str=F.text();
			columns = parseRecipeFormatColumns(recipeFormat);
			numberOfDataColumns = 0;
			for(int c = 0; c < columns.size(); c++)
			{
				if(columns.elementAt(c) instanceof List)
					numberOfDataColumns++;
			}
			dataRows = null;
			try
			{
				dataRows = parseDataRows(str,columns,numberOfDataColumns);
				final DVector editRow = new DVector(2);
				for(int c=0;c<columns().size();c++)
				{
					if(columns().elementAt(c) instanceof List)
						editRow.addElement(columns().elementAt(c),"");
				}
				if(editRow.size()==0)
				{
					//classFieldIndex = CMAbleParms.getClassFieldIndex(dataRow);
				}
				else
					classFieldIndex = CMAbleParms.getClassFieldIndex(editRow);
				fixDataColumns(dataRows);
			}
			catch(final CMException e)
			{
				parseError = e.getMessage();
				return;
			}
			columnLengths = new int[numberOfDataColumns];
			columnHeaders = new String[numberOfDataColumns];
			calculateRecipeCols(columnLengths,columnHeaders,dataRows);
		}

		@Override
		public boolean wasVFS()
		{
			return wasVFS;
		}

		@Override
		public DVector newRow(String classFieldData)
		{
			final DVector editRow = blankRow();
			final int keyIndex =classFieldIndex;
			if((keyIndex>=0)&&(classFieldData!=null))
			{
				editRow.setElementAt(keyIndex,2,classFieldData);
			}
			try
			{
				fixDataColumn(editRow,-1);
			}
			catch (final CMException cme)
			{
				return null;
			}
			for(int i=0;i<editRow.size();i++)
			{
				if(i!=keyIndex)
				{
					final AbilityParmEditor A = getEditors().get(editRow.elementAt(i,1));
					editRow.setElementAt(i,2,A.defaultValue());
				}
			}
			return editRow;
		}
		
		@Override
		public DVector blankRow()
		{
			final DVector editRow = new DVector(2);
			for(int c=0;c<columns().size();c++)
			{
				if(columns().elementAt(c) instanceof List)
					editRow.addElement(columns().elementAt(c),"");
			}
			return editRow;
		}

		@Override
		public int getClassFieldIndex()
		{
			return classFieldIndex;
		}

		@Override
		public String recipeFilename()
		{
			return recipeFilename;
		}

		@Override
		public String recipeFormat()
		{
			return recipeFormat;
		}

		@Override
		public Vector<DVector> dataRows()
		{
			return dataRows;
		}

		@Override
		public Vector<? extends Object> columns()
		{
			return columns;
		}

		@Override
		public int[] columnLengths()
		{
			return columnLengths;
		}

		@Override
		public String[] columnHeaders()
		{
			return columnHeaders;
		}

		@Override
		public int numberOfDataColumns()
		{
			return numberOfDataColumns;
		}

		@Override
		public String parseError()
		{
			return parseError;
		}
	}

	protected abstract class AbilityParmEditorImpl implements AbilityParmEditor
	{
		private final String ID;
		private PairList<String,String> choices = null;
		private final ParmType fieldType;
		private String prompt = null;
		private String header = null;

		public AbilityParmEditorImpl(String fieldName, String shortHeader, ParmType type)
		{
			ID=fieldName;
			fieldType = type;
			header = shortHeader;
			prompt = CMStrings.capitalizeAndLower(CMStrings.replaceAll(ID,"_"," "));
			createChoices();
		}

		@Override
		public String ID()
		{
			return ID;
		}

		@Override
		public ParmType parmType()
		{
			return fieldType;
		}

		@Override
		public String prompt()
		{
			return prompt;
		}

		@Override
		public String colHeader()
		{
			return header;
		}

		@Override
		public boolean confirmValue(String oldVal)
		{
			final boolean spaceOK = fieldType != ParmType.ONEWORD;
			boolean emptyOK = false;
			switch(fieldType)
			{
			case STRINGORNULL:
				emptyOK = true;
			//$FALL-THROUGH$
			case ONEWORD:
			case STRING:
			{
				if((!spaceOK) && (oldVal.indexOf(' ') >= 0))
					return false;
				return (emptyOK)||(oldVal.trim().length()>0);
			}
			case NUMBER:
				return CMath.isInteger(oldVal);
			case CHOICES:
				if(!CMStrings.contains(choices.toArrayFirst(new String[0]),oldVal))
					return CMStrings.contains(choices.toArrayFirst(new String[0]),oldVal.toUpperCase().trim());
				return true;
			case MULTICHOICES:
				return CMath.isInteger(oldVal)||choices().contains(oldVal);
			case SPECIAL:
				break;
			}
			return false;
		}

		@Override
		public String[] fakeUserInput(String oldVal)
		{
			boolean emptyOK = false;
			switch(fieldType)
			{
			case STRINGORNULL:
				emptyOK = true;
			//$FALL-THROUGH$
			case ONEWORD:
			case STRING:
			{
				if(emptyOK && (oldVal.trim().length()==0))
					return new String[]{"NULL"};
				return new String[]{oldVal};
			}
			case NUMBER:
				return new String[]{oldVal};
			case CHOICES:
			{
				if(oldVal.trim().length()==0) return new String[]{"NULL"};
				final Vector<String> V = new XVector<String>(choices.toArrayFirst(new String[0]));
				for(int v=0;v<V.size();v++)
				{
					if(oldVal.equalsIgnoreCase(V.elementAt(v)))
						return new String[]{choices.get(v).second};
				}
				return new String[]{oldVal};
			}
			case MULTICHOICES:
				if(oldVal.trim().length()==0) 
					return new String[]{"NULL"};
				if(!CMath.isInteger(oldVal))
				{
					final Vector<String> V = new XVector<String>(choices.toArrayFirst(new String[0]));
					for(int v=0;v<V.size();v++)
					{
						if(oldVal.equalsIgnoreCase(V.elementAt(v)))
							return new String[]{choices.get(v).second,""};
					}
				}
				else
				{
					final Vector<String> V = new Vector<String>();
					for(int c=0;c<choices.size();c++)
					{
						if(CMath.bset(CMath.s_int(oldVal),CMath.s_int(choices.get(c).first)))
						{
							V.addElement(choices.get(c).second);
							V.addElement(choices.get(c).second);
						}
					}
					if(V.size()>0)
					{
						V.addElement("");
						return CMParms.toStringArray(V);
					}
				}
				return new String[]{"NULL"};
			case SPECIAL:
				break;
			}
			return new String[]{};
		}

		@Override
		public String commandLinePrompt(MOB mob, String oldVal, int[] showNumber, int showFlag)
		throws java.io.IOException
		{
			String str = null;
			boolean emptyOK = false;
			final boolean spaceOK = fieldType != ParmType.ONEWORD;
			switch(fieldType)
			{
			case STRINGORNULL:
				emptyOK = true;
			//$FALL-THROUGH$
			case ONEWORD:
			case STRING:
			{
				++showNumber[0];
				boolean proceed = true;
				while(proceed&&(!mob.session().isStopped()))
				{
					str = CMLib.genEd().prompt(mob,oldVal,showNumber[0],showFlag,prompt(),emptyOK).trim();
					if((!spaceOK) && (str.indexOf(' ') >= 0))
						mob.tell(L("Spaces are not allowed here."));
					else
						proceed=false;
				}
				break;
			}
			case NUMBER:
			{
				final String newStr=CMLib.genEd().prompt(mob,oldVal,++showNumber[0],showFlag,prompt(),true);
				if(newStr.trim().length()==0)
					str="";
				else
					str = Integer.toString(CMath.s_int(newStr));
				break;
			}
			case CHOICES:
				str = CMLib.genEd().promptMultiOrExtra(mob,oldVal,++showNumber[0],showFlag,prompt(),choices);
				break;
			case MULTICHOICES:
				str = CMLib.genEd().promptMultiOrExtra(mob,oldVal,++showNumber[0],showFlag,prompt(),choices);
				if(CMath.isInteger(str))
					str = Integer.toString(CMath.s_int(str));
				break;
			case SPECIAL:
				break;
			}
			return str;
		}

		@Override
		public String webValue(HTTPRequest httpReq, java.util.Map<String,String> parms, String oldVal, String fieldName)
		{
			final String webValue = httpReq.getUrlParameter(fieldName);
			switch(fieldType)
			{
			case ONEWORD:
			case STRINGORNULL:
			case STRING:
			case NUMBER:
				return (webValue == null)?oldVal:webValue;
			case MULTICHOICES:
			{
				if(webValue == null)
					return oldVal;
				String id="";
				long num=0;
				int index=0;
				for(;httpReq.isUrlParameter(fieldName+id);id=""+(++index))
				{
					final String newVal = httpReq.getUrlParameter(fieldName+id);
					if(CMath.s_long(newVal)<=0)
						return newVal;
					num |= CMath.s_long(newVal);
				}
				return ""+num;
			}
			case CHOICES:
				return (webValue == null)?oldVal:webValue;
			case SPECIAL:
				break;
			}
			return "";
		}

		@Override
		public String webTableField(HTTPRequest httpReq, java.util.Map<String, String> parms, String oldVal)
		{
			return oldVal;
		}

		@Override
		public String webField(HTTPRequest httpReq, java.util.Map<String,String> parms, String oldVal, String fieldName)
		{
			int textSize = 50;
			final String webValue = webValue(httpReq,parms,oldVal,fieldName);
			String onChange = null;
			final Vector<String> choiceValues = new Vector<String>();
			switch(fieldType)
			{
			case ONEWORD:
				textSize = 10;
			//$FALL-THROUGH$
			case STRINGORNULL:
			case STRING:
				return "\n\r<INPUT TYPE=TEXT NAME=" + fieldName + " SIZE=" + textSize + " VALUE=\"" + webValue + "\">";
			case NUMBER:
				return "\n\r<INPUT TYPE=TEXT NAME=" + fieldName + " SIZE=10 VALUE=\"" + webValue + "\">";
			case MULTICHOICES:
			{
				onChange = " MULTIPLE ";
				if(!parms.containsKey("NOSELECT"))
					onChange+= "ONCHANGE=\"MultiSelect(this);\"";
				if(CMath.isInteger(webValue))
				{
					final int bits = CMath.s_int(webValue);
					for(int i=0;i<choices.size();i++)
					{
						final int bitVal =CMath.s_int(choices.get(i).first);
						if((bitVal>0)&&(CMath.bset(bits,bitVal)))
							choiceValues.addElement(choices.get(i).first);
					}
				}
			}
			//$FALL-THROUGH$
			case CHOICES:
			{
				if(choiceValues.size()==0)
					choiceValues.addElement(webValue);
				if((onChange == null)&&(!parms.containsKey("NOSELECT")))
					onChange = " ONCHANGE=\"Select(this);\"";
				else
				if(onChange==null)
					onChange="";
				final StringBuffer str= new StringBuffer("");
				str.append("\n\r<SELECT NAME="+fieldName+onChange+">");
				for(int i=0;i<choices.size();i++)
				{
					final String option = (choices.get(i).first);
					str.append("<OPTION VALUE=\""+option+"\" ");
					for(int c=0;c<choiceValues.size();c++)
					{
						if(option.equalsIgnoreCase(choiceValues.elementAt(c)))
							str.append("SELECTED");
					}
					str.append(">"+(choices.get(i).second));
				}
				return str.toString()+"</SELECT>";
			}
			case SPECIAL:
				break;
			}
			return "";
		}

		public abstract void createChoices();

		@Override
		public PairList<String,String> createChoices(Enumeration<? extends Object> e)
		{
			if(choices != null)
				return choices;
			choices = new PairVector<String,String>();
			Object o = null;
			for(;e.hasMoreElements();)
			{
				o = e.nextElement();
				if(o instanceof String)
					choices.add((String)o,CMStrings.capitalizeAndLower((String)o));
				else
				if(o instanceof Ability)
					choices.add(((Ability)o).ID(),((Ability)o).name());
				else
				if(o instanceof Race)
					choices.add(((Race)o).ID(),((Race)o).name());
				else
				if(o instanceof Environmental)
					choices.add(((Environmental)o).ID(),((Environmental)o).ID());
			}
			return choices;
		}

		@Override
		public PairList<String,String> createChoices(Vector<? extends Object> V)
		{
			return createChoices(V.elements());
		}

		@Override
		public PairList<String,String> createChoices(String[] S)
		{
			final XVector<String> X=new XVector<String>(S);
			Collections.sort(X);
			return createChoices(X.elements());
		}

		public PairList<String,String> createBinaryChoices(String[] S) 
		{
			if(choices != null)
				return choices;
			choices = createChoices(new XVector<String>(S).elements());
			for(int i=0;i<choices.size();i++)
			{
				if(i==0)
					choices.get(i).first =Integer.toString(0);
				else
					choices.get(i).first = Integer.toString(1<<(i-1));
			}
			return choices;
		}

		public PairList<String,String> createNumberedChoices(String[] S) 
		{
			if(choices != null)
				return choices;
			choices = createChoices(new XVector<String>(S).elements());
			for(int i=0;i<choices.size();i++)
				choices.get(i).first = Integer.toString(i);
			return choices;
		}