ThreadTracker + Attempt on NPC Disappearing fix

Engineered the ThreadTracker: server-embedded deadlock auditing tool, which will print error messages in case of found deadlocks (also showing all in-use locks on the time of the issue).
Changed the player's id on DB now starting from 20mil, thus preventing players from overwriting NPC/mobs with same oid in-game. Requires proper testing to see if the issue has been cleared.
This commit is contained in:
ronancpl
2017-11-16 12:22:32 -02:00
parent aecc3e300a
commit 2b38b62683
50 changed files with 1099 additions and 123 deletions

View File

@@ -43,13 +43,14 @@ import client.MapleClient;
import constants.ServerConstants;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import tools.locks.MonitoredReentrantLock;
import java.util.concurrent.ScheduledFuture;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import server.TimerManager;
import tools.locks.MonitoredEnums;
public class MapleServerHandler extends IoHandlerAdapter {
@@ -58,8 +59,8 @@ public class MapleServerHandler extends IoHandlerAdapter {
private static final SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm");
private static AtomicLong sessionId = new AtomicLong(7777);
private Lock idleLock = new ReentrantLock(true);
private Lock tempLock = new ReentrantLock(true);
private Lock idleLock = new MonitoredReentrantLock(MonitoredEnums.SHANDLER_IDLE, true);
private Lock tempLock = new MonitoredReentrantLock(MonitoredEnums.SHANDLER_TEMP, true);
private Map<MapleClient, Long> idleSessions = new HashMap<>(100);
private Map<MapleClient, Long> tempIdleSessions = new HashMap<>();
private ScheduledFuture<?> idleManager = null;

View File

@@ -35,27 +35,28 @@ public class MaplePacketEncoder implements ProtocolEncoder {
public void encode(final IoSession session, final Object message, final ProtocolEncoderOutput out) throws Exception {
final MapleClient client = (MapleClient) session.getAttribute(MapleClient.CLIENT_KEY);
if (client != null) {
final MapleAESOFB send_crypto = client.getSendCrypto();
final byte[] input = (byte[]) message;
final byte[] unencrypted = new byte[input.length];
System.arraycopy(input, 0, unencrypted, 0, input.length);
final byte[] ret = new byte[unencrypted.length + 4];
final byte[] header = send_crypto.getPacketHeader(unencrypted.length);
MapleCustomEncryption.encryptData(unencrypted);
try {
client.lockClient();
try {
final MapleAESOFB send_crypto = client.getSendCrypto();
final byte[] input = (byte[]) message;
final byte[] unencrypted = new byte[input.length];
System.arraycopy(input, 0, unencrypted, 0, input.length);
final byte[] ret = new byte[unencrypted.length + 4];
final byte[] header = send_crypto.getPacketHeader(unencrypted.length);
MapleCustomEncryption.encryptData(unencrypted);
send_crypto.crypt(unencrypted);
System.arraycopy(header, 0, ret, 0, 4);
System.arraycopy(unencrypted, 0, ret, 4, unencrypted.length);
out.write(IoBuffer.wrap(ret));
} finally {
client.unlockClient();
}
// System.arraycopy(unencrypted, 0, ret, 4, unencrypted.length);
// out.write(ByteBuffer.wrap(ret));
} else {
} catch (NullPointerException npe) {
out.write(IoBuffer.wrap(((byte[]) message)));
}
}

View File

@@ -25,7 +25,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import tools.locks.MonitoredEnums;
import tools.locks.MonitoredReentrantLock;
/**
*
@@ -33,7 +34,7 @@ import java.util.concurrent.locks.ReentrantLock;
*/
public class PlayerBuffStorage {
private int id = (int) (Math.random() * 100);
private final Lock lock = new ReentrantLock(true);
private final Lock lock = new MonitoredReentrantLock(MonitoredEnums.BUFF_STORAGE, true);
private Map<Integer, List<PlayerBuffValueHolder>> buffs = new HashMap<>();
public void addBuffsToStorage(int chrid, List<PlayerBuffValueHolder> toStore) {

View File

@@ -26,12 +26,14 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import tools.locks.MonitoredReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import tools.locks.MonitoredEnums;
public class PlayerStorage {
private final ReentrantReadWriteLock locks = new ReentrantReadWriteLock(true);
private final ReentrantReadWriteLock locks = new MonitoredReentrantReadWriteLock(MonitoredEnums.PLAYER_STORAGE, true);
private final ReadLock rlock = locks.readLock();
private final WriteLock wlock = locks.writeLock();
private final Map<Integer, MapleCharacter> storage = new LinkedHashMap<>();

View File

@@ -33,15 +33,16 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import tools.locks.MonitoredReentrantLock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.ScheduledFuture;
import net.MapleServerHandler;
import net.mina.MapleCodecFactory;
@@ -70,11 +71,13 @@ import client.SkillFactory;
import constants.ItemConstants;
import constants.ServerConstants;
import java.util.Calendar;
import net.server.audit.ThreadTracker;
import server.quest.MapleQuest;
import tools.locks.MonitoredEnums;
public class Server implements Runnable {
private static final Set<Integer> activeFly = new HashSet<>();
private static final Map<Integer, Integer> couponRates = new LinkedHashMap<>();
private static final Map<Integer, Integer> couponRates = new HashMap<>(30);
private static final List<Integer> activeCoupons = new LinkedList<>();
private IoAcceptor acceptor;
@@ -83,11 +86,12 @@ public class Server implements Runnable {
private final Properties subnetInfo = new Properties();
private static Server instance = null;
private List<Pair<Integer, String>> worldRecommendedList = new LinkedList<>();
private final Map<Integer, MapleGuild> guilds = new LinkedHashMap<>();
private final Map<MapleClient, Long> inLoginState = new LinkedHashMap<>();
private final Lock srvLock = new ReentrantLock();
private final Map<Integer, MapleGuild> guilds = new HashMap<>(100);
private final Map<MapleClient, Long> inLoginState = new HashMap<>(100);
private final Lock srvLock = new MonitoredReentrantLock(MonitoredEnums.SERVER);
private final PlayerBuffStorage buffStorage = new PlayerBuffStorage();
private final Map<Integer, MapleAlliance> alliances = new LinkedHashMap<>();
private final Map<Integer, MapleAlliance> alliances = new HashMap<>(100);
private boolean online = false;
public static long uptime = System.currentTimeMillis();
@@ -301,7 +305,9 @@ public class Server implements Runnable {
timeToTake = System.currentTimeMillis();
MapleQuest.loadAllQuest();
System.out.println("Quest loaded in " + ((System.currentTimeMillis() - timeToTake) / 1000.0) + " seconds\r\n");
if(ServerConstants.USE_THREAD_TRACKER) ThreadTracker.getInstance().registerThreadTrackerTask();
try {
Integer worldCount = Math.min(ServerConstants.WORLD_NAMES.length, Integer.parseInt(p.getProperty("worlds")));
@@ -317,7 +323,7 @@ public class Server implements Runnable {
worldRecommendedList.add(new Pair<>(i, p.getProperty("whyamirecommended" + i)));
worlds.add(world);
channels.add(new LinkedHashMap<Integer, String>());
channels.add(new HashMap<Integer, String>());
for (int j = 0; j < Integer.parseInt(p.getProperty("channels" + i)); j++) {
int channelid = j + 1;
Channel channel = new Channel(i, channelid);
@@ -807,6 +813,8 @@ public class Server implements Runnable {
}
}
}*/
if(ServerConstants.USE_THREAD_TRACKER) ThreadTracker.getInstance().cancelThreadTrackerTask();
TimerManager.getInstance().purge();
TimerManager.getInstance().stop();

View File

@@ -0,0 +1,281 @@
/*
* This file is part of the MapleSolaxiaV2 Maple Story Server
*
* Copyright (C) 2017 RonanLana
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.server.audit;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import server.TimerManager;
import tools.FilePrinter;
import tools.locks.MonitoredEnums;
import constants.ServerConstants;
/**
*
* @author RonanLana
*
* This tool has the main purpose of auditing deadlocks throughout the server and must be used only for debugging. The flag is USE_THREAD_TRACKER.
*/
public class ThreadTracker {
private static ThreadTracker instance = null;
private final Lock ttLock = new ReentrantLock(true);
private final Map<Long, List<MonitoredEnums>> threadTracker = new HashMap<>();
private final Map<Long, Integer> threadUpdate = new HashMap<>();
private final Map<Long, Thread> threads = new HashMap<>();
private final Map<Long, AtomicInteger> lockCount = new HashMap<>();
private final Map<Long, MonitoredEnums> lockIds = new HashMap<>();
private final Map<Long, Long> lockThreads = new HashMap<>();
private final Map<Long, Byte> lockUpdate = new HashMap<>();
private final Map<MonitoredEnums, Map<Long, Integer>> locks = new HashMap<>();
ScheduledFuture<?> threadTrackerSchedule;
private String printThreadTrackerState(String dateFormat) {
Map<MonitoredEnums, List<Integer>> lockValues = new HashMap<>();
Set<Long> executingThreads = new HashSet<>();
for(Map.Entry<Long, AtomicInteger> lock : lockCount.entrySet()) {
if(lock.getValue().get() != 0) {
executingThreads.add(lockThreads.get(lock.getKey()));
MonitoredEnums lockId = lockIds.get(lock.getKey());
List<Integer> list = lockValues.get(lockId);
if(list == null) {
list = new ArrayList<>();
lockValues.put(lockId, list);
}
list.add(lock.getValue().get());
}
}
String s = "----------------------------\r\n" + dateFormat + "\r\n ";
s += "Lock-thread usage count:";
for(Map.Entry<MonitoredEnums, List<Integer>> lock : lockValues.entrySet()) {
s += ("\r\n " + lock.getKey().name() + ": ");
for(Integer i : lock.getValue()) {
s += (i + " ");
}
}
s += "\r\n\r\nThread opened lock path:";
for(Long tid : executingThreads) {
s += "\r\n";
for(MonitoredEnums lockid : threadTracker.get(tid)) {
s += (lockid.name() + " ");
}
s += "|";
}
s += "\r\n\r\n";
return s;
}
private static String printThreadLog(List<MonitoredEnums> stillLockedPath, String dateFormat) {
String s = "----------------------------\r\n" + dateFormat + "\r\n ";
for(MonitoredEnums lock : stillLockedPath) {
s += (lock.name() + " ");
}
s += "\r\n\r\n";
return s;
}
private static String printThreadStack(StackTraceElement[] list, String dateFormat) {
String s = "----------------------------\r\n" + dateFormat + "\r\n";
for(int i = 0; i < list.length; i++) {
s += (" " + list[i].toString() + "\r\n");
}
return s;
}
public void accessThreadTracker(boolean update, boolean lock, MonitoredEnums lockId, long lockOid) {
ttLock.lock();
try {
if(update) {
if(!lock) { // update tracker
List<Long> toRemove = new ArrayList<>();
for(Long l : threadUpdate.keySet()) {
int next = threadUpdate.get(l) + 1;
if(next == 4) {
List<MonitoredEnums> tt = threadTracker.get(l);
if(tt.isEmpty()) {
toRemove.add(l);
} else {
DateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone(ServerConstants.TIMEZONE));
String df = dateFormat.format(new Date());
FilePrinter.print(FilePrinter.DEADLOCK_LOCKS, printThreadLog(tt, df));
FilePrinter.print(FilePrinter.DEADLOCK_STACK, printThreadStack(threads.get(l).getStackTrace(), df));
}
}
threadUpdate.put(l, next);
}
for(Long l : toRemove) {
threadTracker.remove(l);
threadUpdate.remove(l);
threads.remove(l);
for(Map<Long, Integer> threadLock : locks.values()) {
threadLock.remove(l);
}
}
toRemove.clear();
for(Entry<Long, Byte> it : lockUpdate.entrySet()) {
byte val = (byte)(it.getValue() + 1);
if(val < 60) { // free the structure after 60 silent updates
lockUpdate.put(it.getKey(), val);
} else {
toRemove.add(it.getKey());
}
}
for(Long l : toRemove) {
lockCount.remove(l);
lockIds.remove(l);
lockThreads.remove(l);
lockUpdate.remove(l);
}
} else { // print status
DateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone(ServerConstants.TIMEZONE));
FilePrinter.printError(FilePrinter.DEADLOCK_STATE, printThreadTrackerState(dateFormat.format(new Date())));
//FilePrinter.printError(FilePrinter.DEADLOCK_STATE, "[" + dateFormat.format(new Date()) + "] Presenting current lock path for lockid " + lockId.name() + ".\r\n" + printLockStatus(lockId) + "\r\n-------------------------------\r\n");
}
} else {
long tid = Thread.currentThread().getId();
if(lock) {
AtomicInteger c = lockCount.get(lockOid);
if(c == null) {
c = new AtomicInteger(0);
lockCount.put(lockOid, c);
lockIds.put(lockOid, lockId);
lockThreads.put(lockOid, tid);
lockUpdate.put(lockOid, (byte) 0);
}
c.incrementAndGet();
List<MonitoredEnums> list = threadTracker.get(tid);
if(list == null) {
list = new ArrayList<>(20);
threadTracker.put(tid, list);
threadUpdate.put(tid, 0);
threads.put(tid, Thread.currentThread());
}
list.add(lockId);
Map<Long, Integer> threadLock = locks.get(lockId);
if(threadLock == null) {
threadLock = new HashMap<>(5);
locks.put(lockId, threadLock);
}
Integer lc = threadLock.get(tid);
if(lc != null) {
threadLock.put(tid, lc + 1);
} else {
threadLock.put(tid, 1);
}
}
else {
AtomicInteger c = lockCount.get(lockOid);
c.decrementAndGet();
lockUpdate.put(lockOid, (byte) 0);
List<MonitoredEnums> list = threadTracker.get(tid);
for(int i = list.size() - 1; i >= 0; i--) {
if(lockId.getValue() == list.get(i).getValue()) {
list.remove(i);
break;
}
}
Map<Long, Integer> threadLock = locks.get(lockId);
threadLock.put(tid, threadLock.get(tid) - 1);
}
}
} finally {
ttLock.unlock();
}
}
private String printLockStatus(MonitoredEnums lockId) {
String s = "";
for(Long threadid : locks.get(lockId).keySet()) {
for(MonitoredEnums lockid : threadTracker.get(threadid)) {
s += (" " + lockid.name());
}
s += " |\r\n";
}
return s;
}
public void registerThreadTrackerTask() {
threadTrackerSchedule = TimerManager.getInstance().register(new Runnable() {
@Override
public void run() {
accessThreadTracker(true, false, MonitoredEnums.UNDEFINED, -1);
}
}, 10000, 10000);
}
public void cancelThreadTrackerTask() {
threadTrackerSchedule.cancel(false);
}
public static ThreadTracker getInstance() {
if (instance == null) {
instance = new ThreadTracker();
}
return instance;
}
}

View File

@@ -33,7 +33,8 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import tools.locks.MonitoredReentrantLock;
import tools.locks.MonitoredReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
@@ -67,6 +68,7 @@ import tools.MaplePacketCreator;
import client.MapleCharacter;
import constants.ServerConstants;
import server.maps.MapleMiniDungeonInfo;
import tools.locks.MonitoredEnums;
public final class Channel {
@@ -90,11 +92,11 @@ public final class Channel {
private Map<Integer, Integer> dojoParty = new HashMap<>();
private Map<Integer, MapleMiniDungeon> dungeons = new HashMap<>();
private ReentrantReadWriteLock merchantLock = new ReentrantReadWriteLock(true);
private ReentrantReadWriteLock merchantLock = new MonitoredReentrantReadWriteLock(MonitoredEnums.MERCHANT, true);
private ReadLock merchRlock = merchantLock.readLock();
private WriteLock merchWlock = merchantLock.writeLock();
private Lock lock = new ReentrantLock(true);
private Lock lock = new MonitoredReentrantLock(MonitoredEnums.CHANNEL, true);
public Channel(final int world, final int channel) {
this.world = world;

View File

@@ -84,7 +84,7 @@ public final class ReportHandler extends AbstractMaplePacketHandler {
public void addReport(int reporterid, int victimid, int reason, String description, String chatlog) {
Calendar calendar = Calendar.getInstance();
Timestamp currentTimestamp = new java.sql.Timestamp(calendar.getTime().getTime());
Connection con = null;
Connection con;
try {
con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO reports (`reporttime`, `reporterid`, `victimid`, `reason`, `chatlog`, `description`) VALUES (?, ?, ?, ?, ?, ?)");

View File

@@ -51,6 +51,7 @@ import server.life.MobAttackInfoFactory;
import server.life.MobSkill;
import server.life.MobSkillFactory;
import server.maps.MapleMap;
import server.maps.MapleMapObject;
import tools.FilePrinter;
import tools.MaplePacketCreator;
import tools.Randomizer;
@@ -78,9 +79,17 @@ public final class TakeDamageHandler extends AbstractMaplePacketHandler {
oid = slea.readInt();
try {
attacker = (MapleMonster) map.getMapObject(oid);
List<loseItem> loseItems;
MapleMapObject mmo = map.getMapObject(oid);
if(mmo instanceof MapleMonster) {
attacker = (MapleMonster) mmo;
if(attacker.getId() != monsteridfrom) {
attacker = null;
}
}
if (attacker != null) {
List<loseItem> loseItems;
if (attacker.isBuffed(MonsterStatus.NEUTRALISE)) {
return;
}
@@ -119,10 +128,8 @@ public final class TakeDamageHandler extends AbstractMaplePacketHandler {
} catch(ClassCastException e) {
//this happens due to mob on last map damaging player just before changing maps
if(ServerConstants.USE_DEBUG) {
e.printStackTrace();
FilePrinter.printError(FilePrinter.EXCEPTION_CAUGHT, "Attacker is not a mob-type, rather is a " + map.getMapObject(oid).getClass().getName() + " entity.");
}
e.printStackTrace();
FilePrinter.printError(FilePrinter.EXCEPTION_CAUGHT, "Attacker is not a mob-type, rather is a " + map.getMapObject(oid).getClass().getName() + " entity.");
return;
}

View File

@@ -36,12 +36,13 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import tools.locks.MonitoredReentrantLock;
import net.server.Server;
import net.server.channel.Channel;
import tools.DatabaseConnection;
import tools.MaplePacketCreator;
import tools.locks.MonitoredEnums;
public class MapleGuild {
public final static int CREATE_GUILD_COST = 1500000;
@@ -52,7 +53,7 @@ public class MapleGuild {
}
private final List<MapleGuildCharacter> members;
private final Lock membersLock = new ReentrantLock(true);
private final Lock membersLock = new MonitoredReentrantLock(MonitoredEnums.GUILD, true);
private String rankTitles[] = new String[5]; // 1 = master, 2 = jr, 5 = lowest member
private String name, notice;

View File

@@ -30,8 +30,9 @@ import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Map;
import java.util.Comparator;
import java.util.concurrent.locks.ReentrantLock;
import tools.locks.MonitoredReentrantLock;
import java.util.concurrent.locks.Lock;
import tools.locks.MonitoredEnums;
public class MapleParty {
private int id;
@@ -43,7 +44,7 @@ public class MapleParty {
private Map<Integer, Integer> histMembers = new HashMap<>();
private int nextEntry = 0;
private Lock lock = new ReentrantLock(true);
private Lock lock = new MonitoredReentrantLock(MonitoredEnums.PARTY, true);
public MapleParty(int id, MaplePartyCharacter chrfor) {
this.leaderId = chrfor.getId();

View File

@@ -43,7 +43,7 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import tools.locks.MonitoredReentrantLock;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ScheduledFuture;
@@ -68,6 +68,7 @@ import server.maps.AbstractMapleMapObject;
import tools.DatabaseConnection;
import tools.MaplePacketCreator;
import tools.Pair;
import tools.locks.MonitoredEnums;
/**
*
@@ -89,25 +90,25 @@ public class World {
private Map<Integer, MapleParty> parties = new HashMap<>();
private AtomicInteger runningPartyId = new AtomicInteger();
private Lock partyLock = new ReentrantLock(true);
private Lock partyLock = new MonitoredReentrantLock(MonitoredEnums.WORLD_PARTY, true);
private Map<Integer, Integer> owlSearched = new LinkedHashMap<>();
private Lock owlLock = new ReentrantLock();
private Lock owlLock = new MonitoredReentrantLock(MonitoredEnums.WORLD_OWL);
private Lock activePetsLock = new ReentrantLock(true);
private Lock activePetsLock = new MonitoredReentrantLock(MonitoredEnums.WORLD_PETS, true);
private Map<Integer, Byte> activePets = new LinkedHashMap<>();
private ScheduledFuture<?> petsSchedule;
private long petUpdate;
private Lock activeMountsLock = new ReentrantLock(true);
private Lock activeMountsLock = new MonitoredReentrantLock(MonitoredEnums.WORLD_MOUNTS, true);
private Map<Integer, Byte> activeMounts = new LinkedHashMap<>();
private ScheduledFuture<?> mountsSchedule;
private long mountUpdate;
private Lock activePlayerShopsLock = new ReentrantLock(true);
private Lock activePlayerShopsLock = new MonitoredReentrantLock(MonitoredEnums.WORLD_PSHOPS, true);
private Map<Integer, MaplePlayerShop> activePlayerShops = new LinkedHashMap<>();
private Lock activeMerchantsLock = new ReentrantLock(true);
private Lock activeMerchantsLock = new MonitoredReentrantLock(MonitoredEnums.WORLD_MERCHS, true);
private Map<Integer, Pair<MapleHiredMerchant, Byte>> activeMerchants = new LinkedHashMap<>();
private long merchantUpdate;