/
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.exceptions.BadEmailAddressException;
import com.planet_ink.coffee_mud.core.interfaces.*;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.CMSecurity.DbgFlag;
import com.planet_ink.coffee_mud.core.collections.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.PlayerLibrary.ThinPlayer;
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.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.util.*;
/*
   Copyright 2008-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 CMPlayers extends StdLibrary implements PlayerLibrary
{
	@Override public String ID(){return "CMPlayers";}

	protected SVector<MOB> 				playersList			= new SVector<MOB>();
	protected SVector<PlayerAccount>	accountsList		= new SVector<PlayerAccount>();
	protected boolean					allAccountsLoaded	= false;
	protected CrossRefTreeMap<MOB,Room> playerLocations		= new CrossRefTreeMap<MOB,Room>(Integer.MAX_VALUE,1);
	protected long[] 					autoPurgeDaysLevels	= new long[1];
	protected long[] 					prePurgeLevels		= new long[1];
	protected int						autoPurgeHash		= 0;

	protected final static int			PRIDE_TOP_SIZE		= 10;
	protected final long[]				topPrideExpiration	= new long[TimeClock.TimePeriod.values().length];
	@SuppressWarnings("unchecked")
	protected final List<Pair<String,Integer>>[][] topPlayers	 = new List[TimeClock.TimePeriod.values().length][AccountStats.PrideStat.values().length];
	@SuppressWarnings("unchecked")
	protected final List<Pair<String,Integer>>[][] topAccounts	 = new List[TimeClock.TimePeriod.values().length][AccountStats.PrideStat.values().length];

	protected final static List<Pair<String,Integer>> emptyPride = new ReadOnlyVector<Pair<String,Integer>>(1);


	@Override
	public int numPlayers()
	{
		return playersList.size();
	}

	@Override
	public synchronized void addPlayer(MOB newOne)
	{
		if(getPlayer(newOne.Name())!=null)
			return;
		if(playersList.contains(newOne))
			return;
		PlayerAccount acct = null;
		if(newOne.playerStats()!=null)
			acct=newOne.playerStats().getAccount();
		playersList.add(newOne);
		addAccount(acct);
	}

	@Override
	public synchronized void delPlayer(MOB oneToDel)
	{
		if(oneToDel != null)
		{
			playersList.remove(oneToDel);
			playerLocations.removeFirst(oneToDel);
		}
	}

	@Override
	public Set<MOB> getPlayersHere(Room room)
	{
		return playerLocations.getSecond(room);
	}

	@Override
	public void changePlayersLocation(MOB mob, Room room)
	{
		if(mob != null)
		{
			if(room == null)
				playerLocations.removeFirst(mob);
			else
				playerLocations.change(mob, room);
		}
	}


	@Override
	public MOB getLoadPlayerByEmail(String email)
	{
		for(final Enumeration<MOB> e=players();e.hasMoreElements();)
		{
			final MOB M=e.nextElement();
			if((M!=null)&&(M.playerStats()!=null)&&(M.playerStats().getEmail().equalsIgnoreCase(email)))
				return M;
		}
		for(final Enumeration<ThinPlayer> e=thinPlayers("",null);e.hasMoreElements();)
		{
			final ThinPlayer P=e.nextElement();
			if(P.email().equalsIgnoreCase(email))
				return getLoadPlayer(P.name());
		}
		return null;
	}

	@Override
	public PlayerAccount getLoadAccount(String calledThis)
	{
		PlayerAccount A = getAccount(calledThis);
		if(A!=null)
			return A;
		if(allAccountsLoaded)
			return null;
		A=CMLib.database().DBReadAccount(calledThis);
		if(A!=null)
			addAccount(A);
		return A;
	}

	@Override
	public synchronized void addAccount(PlayerAccount acct)
	{
		if(acct==null)
			return;
		if(accountsList.contains(acct))
			return;
		for(final PlayerAccount A : accountsList) // dont consolodate this.
		{
			if(A.getAccountName().equals(acct.getAccountName()))
				return;
		}
		accountsList.add(acct);
	}

	@Override
	public PlayerAccount getLoadAccountByEmail(String email)
	{
		if(!CMProps.isUsingAccountSystem())
			return null;
		for(final Enumeration<PlayerAccount> e=accounts();e.hasMoreElements();)
		{
			final PlayerAccount P=e.nextElement();
			if(P.getEmail().equalsIgnoreCase(email))
				return P;
		}
		for(final Enumeration<PlayerAccount> e=accounts("",null);e.hasMoreElements();)
		{
			final PlayerAccount P=e.nextElement();
			if(P.getEmail().equalsIgnoreCase(email))
				return P;
		}
		return null;
	}

	@Override
	public PlayerAccount getAccount(String calledThis)
	{
		calledThis=CMStrings.capitalizeAndLower(calledThis);
		for(final PlayerAccount A : accountsList)
		{
			if(A.getAccountName().equals(calledThis))
				return A;
		}

		for (final MOB M : playersList)
		{
			if((M.playerStats()!=null)
			&&(M.playerStats().getAccount()!=null)
			&&(M.playerStats().getAccount().getAccountName().equals(calledThis)))
			{
				addAccount(M.playerStats().getAccount());
				return M.playerStats().getAccount();
			}
		}
		return null;
	}

	@Override
	public MOB getPlayer(String calledThis)
	{
		calledThis=CMStrings.capitalizeAndLower(calledThis);
		for (final MOB M : playersList)
		{
			if (M.Name().equals(calledThis))
				return M;
		}
		return null;
	}

	@Override
	public MOB getLoadPlayer(String last)
	{
		if(!CMProps.getBoolVar(CMProps.Bool.MUDSTARTED))
			return null;
		MOB M=getPlayer(last);
		if(M!=null)
			return M;
		if(playerExists(last))
		{
			M=CMLib.database().DBReadPlayer(CMStrings.capitalizeAndLower(last));
			CMLib.database().DBReadFollowers(M,false);
			if(M.playerStats()!=null)
				M.playerStats().setLastUpdated(M.playerStats().getLastDateTime());
			M.recoverPhyStats();
			M.recoverCharStats();
			Ability A=null;
			for(int a=0;a<M.numAbilities();a++)
			{
				A=M.fetchAbility(a);
				if(A!=null)
					A.autoInvocation(M, false);
			}
		}
		return M;
	}

	@Override
	public boolean accountExists(String name)
	{
		if(name==null)
			return false;
		name=CMStrings.capitalizeAndLower(name);
		return getLoadAccount(name)!=null;
	}

	@Override
	public boolean playerExists(String name)
	{
		if(name==null)
			return false;
		name=CMStrings.capitalizeAndLower(name);
		for(final MOB M: playersList)
		{
			if(M.Name().equals(name))
				return true;
		}
		return CMLib.database().DBUserSearch(name)!=null;
	}

	@Override
	public Enumeration<MOB> players()
	{
		return playersList.elements();
	}

	@Override
	public Enumeration<PlayerAccount> accounts()
	{
		return accountsList.elements();
	}

	@Override
	public List<Pair<String,Integer>> getTopPridePlayers(TimeClock.TimePeriod period, AccountStats.PrideStat stat)
	{
		List<Pair<String,Integer>> top=topPlayers[period.ordinal()][stat.ordinal()];
		if(top == null)
			top=emptyPride;
		return top;
	}

	@Override
	public List<Pair<String,Integer>> getTopPrideAccounts(TimeClock.TimePeriod period, AccountStats.PrideStat stat)
	{
		List<Pair<String,Integer>> top=topAccounts[period.ordinal()][stat.ordinal()];
		if(top == null)
			top=emptyPride;
		return top;
	}

	private void removePrideStat(List<Pair<String,Integer>> top, String name, int start)
	{
		for(int i=start;i<top.size();i++)
		{
			if(top.get(i).first.equals(name))
			{
				top.remove(i);
				break;
			}
		}
	}

	@Override
	public int bumpPrideStat(final MOB mob, final AccountStats.PrideStat stat, final int amt)
	{
		if((amt != 0)&&(mob!=null))
		{
			final PlayerStats pstats=mob.playerStats();
			if(pstats != null)
			{
				pstats.bumpPrideStat(stat, amt);
				adjustTopPrideStats(topPlayers,mob.Name(),stat,pstats);
				if(pstats.getAccount() != null)
				{
					pstats.getAccount().bumpPrideStat(stat,amt);
					adjustTopPrideStats(topAccounts,pstats.getAccount().getAccountName(),stat,pstats.getAccount());
				}
				return amt;
			}
		}
		return 0;
	}

	protected void adjustTopPrideStats(final List<Pair<String,Integer>>[][] topWhat, final String name, final AccountStats.PrideStat stat, final AccountStats astats)
	{
		for(final TimeClock.TimePeriod period : TimeClock.TimePeriod.values())
		{
			final List<Pair<String,Integer>> top=topWhat[period.ordinal()][stat.ordinal()];
			if(top == null)
				continue;
			synchronized(top)
			{
				final int pVal=astats.getPrideStat(period, stat);
				if(pVal <= 0)
					removePrideStat(top,name,0);
				else
				{
					boolean found=false;
					for(int i=0;i<top.size();i++)
					{
						if(top.get(i).first.equals(name))
						{
							found=true;
							top.get(i).second=Integer.valueOf(pVal);
							break;
						}
						else
						if(pVal > top.get(i).second.intValue())
						{
							top.add(i,new Pair<String,Integer>(name,Integer.valueOf(pVal)));
							removePrideStat(top,name,i+1);
							found=true;
							break;
						}
					}
					if((!found)&&(top.size()<PRIDE_TOP_SIZE))
						top.add(new Pair<String,Integer>(name,Integer.valueOf(pVal)));
				}
			}
		}
	}

	@Override
	public void renamePlayer(MOB mob, String oldName)
	{
		final String newName = mob.Name();
		CMLib.database().DBPlayerNameChange(oldName, newName);
		for(final Enumeration<MOB> p=CMLib.players().players();p.hasMoreElements();)
		{
			final MOB playerM=p.nextElement();
			if(playerM.getWorshipCharID().equalsIgnoreCase(oldName))
				playerM.setWorshipCharID(newName);
			if(playerM.getLiegeID().equalsIgnoreCase(oldName))
				playerM.setLiegeID(newName);
		}
		for(int q=0;q<CMLib.quests().numQuests();q++)
		{
			final Quest Q=CMLib.quests().fetchQuest(q);
			if(Q.wasWinner(oldName))
			{
				Q.declareWinner("-"+oldName);
				Q.declareWinner(newName);
			}
		}
		final PlayerStats pstats = mob.playerStats();
		if(pstats!=null)
		{
			final PlayerAccount account = pstats.getAccount();
			if(account != null)
			{
				account.delPlayer(oldName);
				account.addNewPlayer(mob);
				CMLib.database().DBUpdateAccount(account);
				account.setLastUpdated(System.currentTimeMillis());
			}
			for(final Enumeration<Item> e=pstats.getExtItems().items(); e.hasMoreElements();)
			{
				final Item I=e.nextElement();
				if(I instanceof PrivateProperty)
					if(((PrivateProperty)I).getOwnerName().equalsIgnoreCase(oldName))
						((PrivateProperty)I).setOwnerName(newName);
			}
		}

		for(final Enumeration<Room> r=CMLib.map().roomsFilled();r.hasMoreElements();)
		{
			Room R=r.nextElement();
			if((R!=null)&&(R.roomID().length()>0))
			{
				synchronized(("SYNC"+R.roomID()).intern())
				{
					R=CMLib.map().getRoom(R);
					boolean changed=false;
					for(final Enumeration<Ability> a=R.effects();a.hasMoreElements();)
					{
						final Ability A=a.nextElement();
						if(A==null)
							continue;
						if(A instanceof LandTitle)
						{
							if(((LandTitle)A).getOwnerName().equals(oldName))
							{
								((LandTitle)A).setOwnerName(newName);
								changed=true;
							}
						}
						else
						if(A.text().equals(oldName))
						{
							changed=true;
							A.setMiscText(newName);
						}
					}
					if(changed)
					{
						CMLib.database().DBUpdateRoom(R);
					}
				}
			}
		}
	}

	@Override
	public void obliteratePlayer(MOB deadMOB, boolean deleteAssets, boolean quiet)
	{
		if(deadMOB==null)
			return;
		if(getPlayer(deadMOB.Name())!=null)
		{
			deadMOB=getPlayer(deadMOB.Name());
			delPlayer(deadMOB);
		}
		for(final Session S : CMLib.sessions().allIterable())
		{
			if((!S.isStopped())&&(S.mob()!=null)&&(S.mob().Name().equals(deadMOB.Name())))
				deadMOB=S.mob();
		}
		if(deadMOB.playerStats()!=null)
		{
			final PlayerAccount A=deadMOB.playerStats().getAccount();
			if(A!=null)
				A.delPlayer(deadMOB);
		}
		final Room deadLoc=deadMOB.location();
		if(deleteAssets)
		{
			final CMMsg msg=CMClass.getMsg(deadMOB,null,CMMsg.MSG_RETIRE,(quiet)?null:L("A horrible death cry is heard throughout the land."));
			if(deadLoc!=null)
				deadLoc.send(deadMOB,msg);
			try
			{
				for(final Enumeration<Room> r=CMLib.map().rooms();r.hasMoreElements();)
				{
					final Room R=r.nextElement();
					if((R!=null)&&(R!=deadLoc))
					{
						if(R.okMessage(deadMOB,msg))
							R.sendOthers(deadMOB,msg);
						else
						{
							addPlayer(deadMOB);
							return;
						}
					}
				}
			}catch(final NoSuchElementException e){}
		}
		final StringBuffer newNoPurge=new StringBuffer("");
		final List<String> protectedOnes=Resources.getFileLineVector(Resources.getFileResource("protectedplayers.ini",false));
		boolean somethingDone=false;
		if((protectedOnes!=null)&&(protectedOnes.size()>0))
		{
			for(int b=0;b<protectedOnes.size();b++)
			{
				final String B=protectedOnes.get(b);
				if(!B.equalsIgnoreCase(deadMOB.name()))
					newNoPurge.append(B+"\n");
				else
					somethingDone=true;
			}
			if(somethingDone)
				Resources.updateFileResource("::protectedplayers.ini",newNoPurge);
		}

		PlayerStats pStats = deadMOB.playerStats();
		if(pStats != null)
			pStats.getExtItems().delAllItems(true);
		CMLib.database().DBDeletePlayer(deadMOB, deleteAssets);
		if(deadMOB.session()!=null)
			deadMOB.session().stopSession(false,false,false);
		Log.sysOut(deadMOB.name()+" has been deleted.");
		deadMOB.destroy();
	}

	@Override
	public synchronized void obliterateAccountOnly(PlayerAccount deadAccount)
	{
		deadAccount = getLoadAccount(deadAccount.getAccountName());
		if(deadAccount==null)
			return;
		accountsList.remove(deadAccount);

		final StringBuffer newNoPurge=new StringBuffer("");
		final List<String> protectedOnes=Resources.getFileLineVector(Resources.getFileResource("protectedplayers.ini",false));
		boolean somethingDone=false;
		if((protectedOnes!=null)&&(protectedOnes.size()>0))
		{
			for(int b=0;b<protectedOnes.size();b++)
			{
				final String B=protectedOnes.get(b);
				if(!B.equalsIgnoreCase(deadAccount.getAccountName()))
					newNoPurge.append(B+"\n");
				else
					somethingDone=true;
			}
			if(somethingDone)
				Resources.updateFileResource("::protectedplayers.ini",newNoPurge);
		}

		CMLib.database().DBDeleteAccount(deadAccount);
		Log.sysOut(deadAccount.getAccountName()+" has been deleted.");
	}

	@Override
	public int savePlayers()
	{
		int processed=0;
		final boolean noCachePlayers=CMProps.getBoolVar(CMProps.Bool.PLAYERSNOCACHE);
		for(final MOB mob : playersList)
		{
			try
			{
				final PlayerStats pStats=mob.playerStats();
				if(!mob.isMonster())
				{
					CMLib.factions().updatePlayerFactions(mob,mob.location(), false);
					setThreadStatus(serviceClient,"just saving "+mob.Name());
					CMLib.database().DBUpdatePlayerMOBOnly(mob);
					if((mob.Name().length()==0)||(pStats==null))
						continue;
					setThreadStatus(serviceClient,"saving "+mob.Name()+", "+mob.numItems()+" items");
					CMLib.database().DBUpdatePlayerItems(mob);
					setThreadStatus(serviceClient,"saving "+mob.Name()+", "+mob.numAbilities()+" abilities");
					CMLib.database().DBUpdatePlayerAbilities(mob);
					setThreadStatus(serviceClient,"saving "+mob.numFollowers()+" followers of "+mob.Name());
					CMLib.database().DBUpdateFollowers(mob);
					final PlayerAccount account = pStats.getAccount();
					pStats.setLastUpdated(System.currentTimeMillis());
					if(account!=null)
					{
						setThreadStatus(serviceClient,"saving account "+account.getAccountName()+" for "+mob.Name());
						CMLib.database().DBUpdateAccount(account);
						account.setLastUpdated(System.currentTimeMillis());
					}
					processed++;
				}
				else
				if(pStats!=null)
				{
					if((pStats.getLastUpdated()==0)
					||(pStats.getLastUpdated()<pStats.getLastDateTime())
					||(noCachePlayers && (!CMLib.flags().isInTheGame(mob, true))))
					{
						setThreadStatus(serviceClient,"just saving "+mob.Name());
						CMLib.database().DBUpdatePlayerMOBOnly(mob);
						if(mob.Name().length()==0)
							continue;
						setThreadStatus(serviceClient,"just saving "+mob.Name()+", "+mob.numItems()+" items");
						CMLib.database().DBUpdatePlayerItems(mob);
						setThreadStatus(serviceClient,"just saving "+mob.Name()+", "+mob.numAbilities()+" abilities");
						CMLib.database().DBUpdatePlayerAbilities(mob);
						pStats.setLastUpdated(System.currentTimeMillis());
						processed++;
					}
					if(noCachePlayers && (!CMLib.flags().isInTheGame(mob, true)))
					{
						delPlayer(mob);
						mob.destroy();
					}
				}
			}
			catch(Throwable t)
			{
				Log.errOut(mob.Name(),t);
			}
		}
		return processed;
	}

	@Override
	public String getThinSortValue(ThinPlayer player, int code)
	{
		switch(code)
		{
		case 0:
			return player.name();
		case 1:
			return player.charClass();
		case 2:
			return player.race();
		case 3:
			return Integer.toString(player.level());
		case 4:
			return Integer.toString(player.age());
		case 5:
			return Long.toString(player.last());
		case 6:
			return player.email();
		case 7:
			return player.ip();
		}
		return player.name();
	}

	public String getThinSortValue(PlayerAccount account, int code)
	{
		switch(code)
		{
		case 0:
			return account.getAccountName();
		case 1:
			return Long.toString(account.getLastDateTime());
		case 2:
			return account.getEmail();
		case 3:
			return account.getLastIP();
		case 4:
			return Integer.toString(account.numPlayers());
		}
		return account.getAccountName();
	}

	@Override
	public int getCharThinSortCode(String codeName, boolean loose)
	{
		int x=CMParms.indexOf(CHAR_THIN_SORT_CODES,codeName);
		if(x<0)
			x=CMParms.indexOf(CHAR_THIN_SORT_CODES2,codeName);
		if(!loose)
			return x;
		if(x<0)
		{
			for(int s=0;s<CHAR_THIN_SORT_CODES.length;s++)
			{
				if(CHAR_THIN_SORT_CODES[s].startsWith(codeName))
					x=s;
			}
		}
		if(x<0)
		{
			for(int s=0;s<CHAR_THIN_SORT_CODES2.length;s++)
			{
				if(CHAR_THIN_SORT_CODES2[s].startsWith(codeName))
					x=s;
			}
		}
		return x;
	}

	public int getAccountThinSortCode(String codeName, boolean loose)
	{
		if((codeName == null)||(codeName.length()==0))
			return -1;
		int x=CMParms.indexOf(ACCOUNT_THIN_SORT_CODES,codeName);
		if(!loose)
			return x;
		if(x<0)
		{
			for(int s=0;s<ACCOUNT_THIN_SORT_CODES.length;s++)
			{
				if(ACCOUNT_THIN_SORT_CODES[s].startsWith(codeName))
					x=s;
			}
		}
		return x;
	}

	@Override
	@SuppressWarnings("unchecked")
	public Enumeration<ThinPlayer> thinPlayers(String sort, Map<String, Object> cache)
	{
		Vector<PlayerLibrary.ThinPlayer> V=(cache==null)?null:(Vector<PlayerLibrary.ThinPlayer>)cache.get("PLAYERLISTVECTOR"+sort);
		if(V==null)
		{
			V=new Vector<PlayerLibrary.ThinPlayer>();
			V.addAll(CMLib.database().getExtendedUserList());
			final int code=getCharThinSortCode(sort,false);
			if((sort.length()>0)
			&&(code>=0)
			&&(V.size()>1))
			{
				final List<PlayerLibrary.ThinPlayer> unV=V;
				V=new Vector<PlayerLibrary.ThinPlayer>();
				while(unV.size()>0)
				{
					ThinPlayer M=unV.get(0);
					String loweStr=getThinSortValue(M,code);
					ThinPlayer lowestM=M;
					for(int i=1;i<unV.size();i++)
					{
						M=unV.get(i);
						final String val=getThinSortValue(M,code);
						if((CMath.isNumber(val)&&CMath.isNumber(loweStr)))
						{
							if(CMath.s_long(val)<CMath.s_long(loweStr))
							{
								loweStr=val;
								lowestM=M;
							}
						}
						else
						if(val.compareTo(loweStr)<0)
						{
							loweStr=val;
							lowestM=M;
						}
					}
					unV.remove(lowestM);
					V.add(lowestM);
				}
			}
			if(cache!=null)
				cache.put("PLAYERLISTVECTOR"+sort,V);
		}
		return V.elements();
	}

	@Override
	@SuppressWarnings("unchecked")
	public Pair<Long,int[]>[] parsePrideStats(final String[] nextPeriods, final String[] prideStats)
	{
		final long now=System.currentTimeMillis();
		final List<Pair<Long,int[]>> finalStats=new ArrayList<Pair<Long,int[]>>(TimeClock.TimePeriod.values().length);
		for(final TimeClock.TimePeriod period : TimeClock.TimePeriod.values())
		{
			final Pair<Long,int[]> p=new Pair<Long,int[]>(Long.valueOf(0),new int[AccountStats.PrideStat.values().length]);
			if(period==TimeClock.TimePeriod.ALLTIME)
				p.first=Long.valueOf(Long.MAX_VALUE);
			else
			if((nextPeriods!=null)&&(nextPeriods.length>period.ordinal())&&(nextPeriods[period.ordinal()].length()>0))
				p.first=Long.valueOf(CMath.s_long(nextPeriods[period.ordinal()]));
			if(now>p.first.longValue())
				p.first=Long.valueOf(period.nextPeriod());
			else
			if((prideStats.length>period.ordinal())&&(prideStats[period.ordinal()].length()>1))
			{
				final String[] prides=prideStats[period.ordinal()].split(",");
				for(final AccountStats.PrideStat stat : AccountStats.PrideStat.values())
				{
					if(prides.length > stat.ordinal())
					{
						final String statVal=prides[stat.ordinal()];
						if(statVal.length()>0)
							p.second[stat.ordinal()]=CMath.s_int(statVal);
					}
				}
			}
			finalStats.add(p);
		}
		return finalStats.toArray(new Pair[0]);
	}

	@Override
	@SuppressWarnings("unchecked")
	public Enumeration<PlayerAccount> accounts(String sort, Map<String, Object> cache)
	{
		Vector<PlayerAccount> V=(cache==null)?null:(Vector<PlayerAccount>)cache.get("ACCOUNTLISTVECTOR"+sort);
		if(V==null)
		{
			V=new Vector<PlayerAccount>();
			if(!allAccountsLoaded)
			{
				if(CMProps.getBoolVar(CMProps.Bool.ACCOUNTSNOCACHE))
					V.addAll(CMLib.database().DBListAccounts(null));
				else
				{
					final List<PlayerAccount> rV=CMLib.database().DBListAccounts(null);
					for(final PlayerAccount A : rV)
						addAccount(A);
					allAccountsLoaded=true;
					V.addAll(accountsList);
				}
			}
			else
				V.addAll(accountsList);
			final int code=getAccountThinSortCode(sort,false);
			if(code<0)
				return V.elements();
			else
			if(V.size()>1)
			{
				final Vector<PlayerAccount> unV=V;
				V=new Vector<PlayerAccount>();
				while(unV.size()>0)
				{
					PlayerAccount A=unV.get(0);
					String loweStr=getThinSortValue(A,code);
					PlayerAccount lowestA=A;
					for(int i=1;i<unV.size();i++)
					{
						A=unV.get(i);
						final String val=getThinSortValue(A,code);
						if((CMath.isNumber(val)&&CMath.isNumber(loweStr)))
						{
							if(CMath.s_long(val)<CMath.s_long(loweStr))
							{
								loweStr=val;
								lowestA=A;
							}
						}
						else
						if(val.compareTo(loweStr)<0)
						{
							loweStr=val;
							lowestA=A;
						}
					}
					unV.remove(lowestA);
					V.add(lowestA);
				}
			}
			if(cache!=null)
				cache.put("ACCOUNTLISTVECTOR"+sort,V);
		}
		return V.elements();
	}

	private boolean isProtected(List<String> protectedOnes, String name)
	{
		boolean protectedOne=false;
		for(int p=0;p<protectedOnes.size();p++)
		{
			final String P=protectedOnes.get(p);
			if(P.equalsIgnoreCase(name))
			{
				protectedOne=true;
				break;
			}
		}
		if(protectedOne)
		{
			if(CMSecurity.isDebugging(CMSecurity.DbgFlag.AUTOPURGE))
				Log.debugOut(serviceClient.getName(),name+" is protected from purging.");
			return true;
		}
		return false;
	}

	private boolean autoPurge()
	{
		if(CMSecurity.isDisabled(CMSecurity.DisFlag.AUTOPURGE))
			return true;
		final String mask=CMProps.getVar(CMProps.Str.AUTOPURGE);
		if(mask.hashCode() != this.autoPurgeHash)
		{
			final int lastLevel=CMProps.getIntVar(CMProps.Int.LASTPLAYERLEVEL)+100;
			final long[][] presorted=CMLib.utensils().compileConditionalRange(CMParms.parseCommas(mask.trim(),true), 2, 0, lastLevel);
			autoPurgeDaysLevels=new long[lastLevel+1];
			prePurgeLevels=new long[lastLevel+1];
			for (int i = 0; i < autoPurgeDaysLevels.length; i++)
				autoPurgeDaysLevels[i] = 0;
			for (int i = 0; i < prePurgeLevels.length; i++)
				prePurgeLevels[i] = 0;
			for(int i=0;i<presorted.length;i++)
			{
				final long[] set=presorted[i];
				if((set==null)||(set.length<2))
				{
					Log.errOut("CMPlayers","Error in AUTOPURGE definition #"+(i+1)+" in coffeemud.ini file! Fix immediately!!");
					continue;
				}
				final long val=set[0];
				if(set[0]<=0)
					continue;
				final long prepurge=set[1];
				long realVal=(val*TimeManager.MILI_DAY);
				long purgePoint=realVal-(prepurge*TimeManager.MILI_DAY);
				if(val <= 0)
				{
					realVal = 0;
					purgePoint = 0;
				}
				if(autoPurgeDaysLevels[i]==0)
					autoPurgeDaysLevels[i]=realVal;
				if(prePurgeLevels[i]==0)
					prePurgeLevels[i]=purgePoint;
			}
			this.autoPurgeHash=mask.hashCode();
		}
		setThreadStatus(serviceClient,"autopurge process");
		final List<PlayerLibrary.ThinPlayer> allUsers=CMLib.database().getExtendedUserList();
		List<String> protectedOnes=Resources.getFileLineVector(Resources.getFileResource("protectedplayers.ini",false));
		if(protectedOnes==null)
			protectedOnes=new Vector<String>();

		for(final ThinPlayer user : allUsers)
		{
			final String name=user.name();
			final int level=user.level();
			final long userLastLoginDateTime=user.last();
			long purgeDateTime;
			long warnDateTime;
			if(level>=autoPurgeDaysLevels.length)
			{
				if(autoPurgeDaysLevels[autoPurgeDaysLevels.length-1]==0)
				{
					if(CMSecurity.isDebugging(CMSecurity.DbgFlag.AUTOPURGE))
						Log.debugOut(serviceClient.getName(),name+" last on "+CMLib.time().date2String(userLastLoginDateTime)+".  Nothing will be done about it.");
					continue;
				}
				purgeDateTime=userLastLoginDateTime + autoPurgeDaysLevels[autoPurgeDaysLevels.length-1];
				warnDateTime=userLastLoginDateTime + prePurgeLevels[prePurgeLevels.length-1];
			}
			else
			if(level>=0)
			{
				if(autoPurgeDaysLevels[level]==0)
				{
					if(CMSecurity.isDebugging(CMSecurity.DbgFlag.AUTOPURGE))
						Log.debugOut(serviceClient.getName(),name+" last on "+CMLib.time().date2String(userLastLoginDateTime)+".  Nothing will be done about it.");
					continue;
				}
				purgeDateTime=userLastLoginDateTime + autoPurgeDaysLevels[level];
				warnDateTime=userLastLoginDateTime + prePurgeLevels[level];
			}
			else
				continue;
			if(CMSecurity.isDebugging(CMSecurity.DbgFlag.AUTOPURGE))
				Log.debugOut(serviceClient.getName(),name+" last on "+CMLib.time().date2String(userLastLoginDateTime)+" will be warned on "+CMLib.time().date2String(warnDateTime)+" and purged on "+CMLib.time().date2String(purgeDateTime));
			if((System.currentTimeMillis()>purgeDateTime)||(System.currentTimeMillis()>warnDateTime))
			{
				if(isProtected(protectedOnes, name))
					continue;

				final List<String> warnedOnes=Resources.getFileLineVector(Resources.getFileResource("warnedplayers.ini",false));
				long foundWarningDateTime=-1;
				final StringBuffer warnStr=new StringBuffer("");
				if((warnedOnes!=null)&&(warnedOnes.size()>0))
				{
					for(int b=0;b<warnedOnes.size();b++)
					{
						final String B=warnedOnes.get(b).trim();
						long warningDateTime=-1;
						if(B.trim().length()>0)
						{
							final int lastSpace=B.lastIndexOf(' ');
							warningDateTime=CMath.s_long(B.substring(lastSpace+1).trim());
							if((warningDateTime > 0) && (System.currentTimeMillis() < warningDateTime + (10 * TimeManager.MILI_DAY)))
								warnStr.append(B+"\n");
							if(B.toUpperCase().startsWith(name.toUpperCase()+" "))
								foundWarningDateTime=warningDateTime;
						}
					}
				}
				if((foundWarningDateTime<0)
				&&(System.currentTimeMillis()>warnDateTime))
				{
					final MOB M=getLoadPlayer(name);
					if((M!=null)&&(M.playerStats()!=null))
					{
						warnStr.append(M.name()+" "+M.playerStats().getEmail()+" "+System.currentTimeMillis()+"\n");
						Resources.updateFileResource("::warnedplayers.ini",warnStr);
						if(CMSecurity.isDebugging(CMSecurity.DbgFlag.AUTOPURGE))
							Log.debugOut(serviceClient.getName(),name+" is now warned.");
						warnPrePurge(M,purgeDateTime-System.currentTimeMillis());
					}
				}
				else
				if((System.currentTimeMillis()>purgeDateTime)
				&&(foundWarningDateTime > 0)
				&&((System.currentTimeMillis()-foundWarningDateTime)>TimeManager.MILI_DAY))
				{
					final MOB M=getLoadPlayer(name);
					if((M!=null)&&(!CMSecurity.isASysOp(M))&&(!CMSecurity.isAllowedAnywhere(M, CMSecurity.SecFlag.NOPURGE)))
					{
						obliteratePlayer(M,true, true);
						M.destroy();
						Log.sysOut(serviceClient.getName(),"AutoPurged user "+name+". Last logged in "+(CMLib.time().date2String(userLastLoginDateTime))+".");
					}
				}
			}
		}

		// accounts!
		if((!CMSecurity.isDisabled(CMSecurity.DisFlag.PURGEACCOUNTS))&&(CMProps.getIntVar(CMProps.Int.ACCOUNTPURGEDAYS)>0))
		{
			for(final Enumeration<PlayerAccount> pe=CMLib.players().accounts("",null); pe.hasMoreElements();)
			{
				final PlayerAccount PA=pe.nextElement();
				if((PA.numPlayers() > 0)
				||(isProtected(protectedOnes, PA.getAccountName())))
					continue;
				final long lastDateTimePurge = PA.getLastDateTime() + (TimeManager.MILI_DAY * CMProps.getIntVar(CMProps.Int.ACCOUNTPURGEDAYS));
				final long lastUpdatedPurge = PA.getLastUpdated() + (TimeManager.MILI_DAY * CMProps.getIntVar(CMProps.Int.ACCOUNTPURGEDAYS));
				final long accountExpPurge = PA.getAccountExpiration() + (TimeManager.MILI_DAY * CMProps.getIntVar(CMProps.Int.ACCOUNTPURGEDAYS));
				long lastTime = lastDateTimePurge;
				if(lastUpdatedPurge > lastTime)
					lastTime=lastUpdatedPurge;
				if(accountExpPurge > lastTime)
					lastTime=accountExpPurge;
				if(System.currentTimeMillis()>lastTime)
				{
					Log.sysOut(serviceClient.getName(),"AutoPurged account "+PA.getAccountName()+".");
					CMLib.players().obliterateAccountOnly(PA);
				}
			}
		}
		return true;
	}

	private void warnPrePurge(MOB mob, long timeLeft)
	{
		// check for valid recipient
		if(mob==null)
			return;

		if((mob.playerStats()==null)
		||(mob.playerStats().getEmail().length()==0)) // no email addy to forward TO
			return;

		//  timeLeft is in millis
		final String from="AutoPurgeWarning";
		final String to=mob.Name();
		final String subj=CMProps.Str.MUDNAME+" Autopurge Warning: "+to;
		String textTimeLeft="";
		if(timeLeft<0)
			timeLeft = 1000*60*60*24;
		if(timeLeft>(1000*60*60*24*2))
		{
			final int days=(int)CMath.div((double)timeLeft,1000*60*60*24);
			textTimeLeft = days + " days";
		}
		else
		{
			final int hours=(int)CMath.div((double)timeLeft,1000*60*60);
			textTimeLeft = hours + " hours";
		}

		final String msg="Your character, "+to+", is going to be autopurged by the system in "+textTimeLeft+".  If you would like to keep this character active, please re-login.  This is an automated message, please do not reply.";

		SMTPLibrary.SMTPClient SC=null;
		try
		{
			if(CMProps.getVar(CMProps.Str.SMTPSERVERNAME).length()>0)
				SC=CMLib.smtp().getClient(CMProps.getVar(CMProps.Str.SMTPSERVERNAME),SMTPLibrary.DEFAULT_PORT);
			else
				SC=CMLib.smtp().getClient(mob.playerStats().getEmail());
		}
		catch(final BadEmailAddressException be)
		{
			Log.errOut(serviceClient.getName(),"Unable to notify "+to+" of impending autopurge.  Invalid email address.");
			return;
		}
		catch(final java.io.IOException ioe)
		{
			return;
		}

		final String replyTo="AutoPurge";
		final String domain=CMProps.getVar(CMProps.Str.MUDDOMAIN).toLowerCase();
		try
		{
			SC.sendMessage(from+"@"+domain,
						   replyTo+"@"+domain,
						   mob.playerStats().getEmail(),
						   mob.playerStats().getEmail(),
						   subj,
						   CMLib.coffeeFilter().simpleOutFilter(msg));
		}
		catch(final java.io.IOException ioe)
		{
			Log.errOut(serviceClient.getName(),"Unable to notify "+to+" of impending autopurge.");
		}
	}

	@Override
	public boolean activate()
	{
		if(serviceClient==null)
		{
			name="THPlayers"+Thread.currentThread().getThreadGroup().getName().charAt(0);
			serviceClient=CMLib.threads().startTickDown(this, Tickable.TICKID_SUPPORT|Tickable.TICKID_SOLITARYMASK, MudHost.TIME_SAVETHREAD_SLEEP, 1);
			CMLib.threads().executeRunnable(new Runnable()
			{
				@Override
				public void run()
				{
					final List<Pair<String,Integer>>[][] pStats = CMLib.database().DBScanPridePlayerWinners(PRIDE_TOP_SIZE, (short)5);
					for(int x=0;x<pStats.length;x++)
						for(int y=0;y<pStats[x].length;y++)
							topPlayers[x][y]=pStats[x][y];
					if(CMProps.isUsingAccountSystem())
					{
						final List<Pair<String,Integer>>[][] aStats = CMLib.database().DBScanPrideAccountWinners(PRIDE_TOP_SIZE, (short)5);
						for(int x=0;x<aStats.length;x++)
						{
							for(int y=0;y<aStats[x].length;y++)
								topAccounts[x][y]=aStats[x][y];
						}
					}
					for(final TimeClock.TimePeriod period : TimeClock.TimePeriod.values())
						topPrideExpiration[period.ordinal()] = period.nextPeriod();
				}

			});
		}
		return true;
	}

	@Override
	public boolean tick(Tickable ticking, int tickID)
	{
		try
		{
			if(!CMSecurity.isDisabled(CMSecurity.DisFlag.PLAYERTHREAD))
			{
				tickStatus=Tickable.STATUS_ALIVE;
				isDebugging=CMSecurity.isDebugging(DbgFlag.PLAYERTHREAD);
				if(checkDatabase() && CMProps.getBoolVar(CMProps.Bool.MUDSTARTED))
				{
					setThreadStatus(serviceClient,"not saving players");
					if((!CMSecurity.isDisabled(CMSecurity.DisFlag.SAVETHREAD))
					&&(!CMSecurity.isDisabled(CMSecurity.DisFlag.PLAYERTHREAD)))
					{
						setThreadStatus(serviceClient,"checking player titles");
						for(final MOB M : playersList)
						{
							if((M.playerStats()!=null)&&(CMLib.flags().isInTheGame(M,true)))
							{
								//boolean didSomething =
								CMLib.titles().evaluateAutoTitles(M);
								//didSomething = 
								CMLib.achievements().evaluateAchievements(M);// || didSomething;
								//if(didSomething)&&(!CMLib.flags().isInTheGame(M,true)))
								//	CMLib.database().DBUpdatePlayerMOBOnly(M);
							}
						}
						autoPurge();
						if(!CMSecurity.isSaveFlag(CMSecurity.SaveFlag.NOPLAYERS))
							savePlayers();
						setThreadStatus(serviceClient,"not saving players");
					}
					setThreadStatus(serviceClient,"expiring top metrics");
					final long now=System.currentTimeMillis();
					List<Pair<String,Integer>> top;
					for(final TimeClock.TimePeriod period : TimeClock.TimePeriod.values())
					{
						if(period == TimeClock.TimePeriod.ALLTIME)
							continue;
						if(now > topPrideExpiration[period.ordinal()])
						{
							topPrideExpiration[period.ordinal()] = period.nextPeriod();
							for(final AccountStats.PrideStat stat : AccountStats.PrideStat.values())
							{
								top=topAccounts[period.ordinal()][stat.ordinal()];
								if(top!=null)
								{
									synchronized(top)
									{
										top.clear();
									}
								}
								top=topPlayers[period.ordinal()][stat.ordinal()];
								if(top!=null)
								{
									synchronized(top)
									{
										top.clear();
									}
								}
							}
						}
					}
					setThreadStatus(serviceClient,"not doing anything");
				}
			}
		}
		finally
		{
			tickStatus=Tickable.STATUS_NOT;
			setThreadStatus(serviceClient,"sleeping");
		}

		return true;
	}

	@Override
	public boolean shutdown()
	{
		playersList.clear();
		playerLocations.clear();
		if(CMLib.threads().isTicking(this, TICKID_SUPPORT|Tickable.TICKID_SOLITARYMASK))
		{
			CMLib.threads().deleteTick(this, TICKID_SUPPORT|Tickable.TICKID_SOLITARYMASK);
			serviceClient=null;
		}
		return true;
	}

	@Override
	public void forceTick()
	{
		serviceClient.tickTicker(false);
	}
}