/
tofu
/
l2interlude
Обзор
Документация
Войти
/
tofu
/
l2interlude
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
src/main/java/com/l2jserver/gameserver/network/L2GameClient.java
940 строк
27 KB
Zoey76
Copyright Update
01 янв 2025, 07:57
01 янв 2025, 07:57
16130a5
Код
Авторство
О чём код?
/* * Copyright © 2004-2025 L2J Server * * This file is part of L2J Server. * * L2J Server 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. * * L2J Server 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 com.l2jserver.gameserver.network; import static com.l2jserver.gameserver.config.Configuration.character; import static com.l2jserver.gameserver.config.Configuration.customs; import static com.l2jserver.gameserver.config.Configuration.general; import static com.l2jserver.gameserver.config.Configuration.mmo; import static com.l2jserver.gameserver.model.PcCondOverride.SEE_ALL_PLAYERS; import static java.util.concurrent.TimeUnit.DAYS; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.l2jserver.commons.database.ConnectionFactory; import com.l2jserver.gameserver.LoginServerThread; import com.l2jserver.gameserver.LoginServerThread.SessionKey; import com.l2jserver.gameserver.ThreadPoolManager; import com.l2jserver.gameserver.data.sql.impl.CharNameTable; import com.l2jserver.gameserver.data.sql.impl.ClanTable; import com.l2jserver.gameserver.data.xml.impl.SecondaryAuthData; import com.l2jserver.gameserver.instancemanager.AntiFeedManager; import com.l2jserver.gameserver.model.CharSelectInfoPackage; import com.l2jserver.gameserver.model.L2Clan; import com.l2jserver.gameserver.model.L2World; import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; import com.l2jserver.gameserver.model.entity.L2Event; import com.l2jserver.gameserver.model.olympiad.OlympiadManager; import com.l2jserver.gameserver.model.zone.ZoneId; import com.l2jserver.gameserver.network.serverpackets.ActionFailed; import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; import com.l2jserver.gameserver.network.serverpackets.ServerClose; import com.l2jserver.gameserver.security.SecondaryPasswordAuth; import com.l2jserver.gameserver.util.FloodProtectors; import com.l2jserver.gameserver.util.Util; import com.l2jserver.mmocore.MMOClient; import com.l2jserver.mmocore.MMOConnection; import com.l2jserver.mmocore.ReceivablePacket; /** * Represents a client connected on Game Server. * @author KenM */ public final class L2GameClient extends MMOClient<MMOConnection<L2GameClient>> implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(L2GameClient.class); private static final Logger LOG_ACCOUNTING = LoggerFactory.getLogger("accounting"); private static final Logger LOG_AUDIT = LoggerFactory.getLogger("audit"); /** * @author KenM */ public enum GameClientState { /** Client has just connected . */ CONNECTED, /** Client has authed but doesn't has character attached to it yet. */ AUTHED, /** Client has selected a character, but it hasn't joined the server yet. */ JOINING, /** Client has selected a char and is in game. */ IN_GAME } private GameClientState _state; // Info private final InetAddress _addr; private String _accountName; private SessionKey _sessionId; private L2PcInstance _activeChar; private final ReentrantLock _activeCharLock = new ReentrantLock(); private SecondaryPasswordAuth _secondaryAuth; private boolean _isAuthedGG; private final long _connectionStartTime; private List<CharSelectInfoPackage> _charSlotMapping = null; private int _charSlot = -1; // flood protectors private final FloodProtectors _floodProtectors = new FloodProtectors(this); // Task protected final ScheduledFuture<?> _autoSaveInDB; protected ScheduledFuture<?> _cleanupTask = null; private L2GameServerPacket _additionalClosePacket; // Crypt private final GameCrypt _crypt; private final ClientStats _stats; private boolean _isDetached = false; private boolean _protocol; private final ArrayBlockingQueue<ReceivablePacket<L2GameClient>> _packetQueue; private final ReentrantLock _queueLock = new ReentrantLock(); private int[][] trace; public L2GameClient(MMOConnection<L2GameClient> con) { super(con); _state = GameClientState.CONNECTED; _connectionStartTime = System.currentTimeMillis(); _crypt = new GameCrypt(); _stats = new ClientStats(); final var capacity = Math.max(general().getClientPacketQueueSize(), mmo().getMaxReadPerPass() + 2); _packetQueue = new ArrayBlockingQueue<>(capacity); if (general().getCharacterDataStoreInterval() > 0) { _autoSaveInDB = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new AutoSaveTask(), 300000L, general().getCharacterDataStoreInterval()); } else { _autoSaveInDB = null; } try { _addr = con != null ? con.getInetAddress() : InetAddress.getLocalHost(); } catch (UnknownHostException e) { throw new Error("Unable to determine localhost address."); } } public byte[] enableCrypt() { byte[] key = BlowFishKeygen.getRandomKey(); _crypt.setKey(key); return key; } public GameClientState getState() { return _state; } public void setState(GameClientState pState) { if (_state != pState) { _state = pState; _packetQueue.clear(); } } public ClientStats getStats() { return _stats; } /** * For loaded offline traders returns localhost address. * @return cached connection IP address, for checking detached clients. */ public InetAddress getConnectionAddress() { return _addr; } public long getConnectionStartTime() { return _connectionStartTime; } @Override public boolean decrypt(ByteBuffer buf, int size) { _crypt.decrypt(buf.array(), buf.position(), size); return true; } @Override public boolean encrypt(final ByteBuffer buf, final int size) { _crypt.encrypt(buf.array(), buf.position(), size); buf.position(buf.position() + size); return true; } public L2PcInstance getActiveChar() { return _activeChar; } public void setActiveChar(L2PcInstance pActiveChar) { _activeChar = pActiveChar; } public ReentrantLock getActiveCharLock() { return _activeCharLock; } public FloodProtectors getFloodProtectors() { return _floodProtectors; } public void setGameGuardOk(boolean val) { _isAuthedGG = val; } public boolean isAuthedGG() { return _isAuthedGG; } public void setAccountName(String pAccountName) { _accountName = pAccountName; if (SecondaryAuthData.getInstance().isEnabled()) { _secondaryAuth = new SecondaryPasswordAuth(this); } } public String getAccountName() { return _accountName; } public void setSessionId(SessionKey sk) { _sessionId = sk; } public SessionKey getSessionId() { return _sessionId; } public void sendPacket(L2GameServerPacket gsp) { if (_isDetached || (gsp == null)) { return; } // Packets from invisible chars sends only to GMs if (gsp.isInvisible() && (getActiveChar() != null) && !getActiveChar().canOverrideCond(SEE_ALL_PLAYERS)) { return; } getConnection().sendPacket(gsp); gsp.runImpl(); } public boolean isDetached() { return _isDetached; } public void setDetached(boolean b) { _isDetached = b; } /** * Method to handle character deletion * @param charslot * @return a byte: * <li>-1: Error: No char was found for such charslot, caught exception, etc... * <li>0: character is not member of any clan, proceed with deletion * <li>1: character is member of a clan, but not clan leader * <li>2: character is clan leader */ public byte markToDeleteChar(int charslot) { int objid = getObjectIdForSlot(charslot); if (objid < 0) { return -1; } try (var con = ConnectionFactory.getInstance().getConnection(); var ps = con.prepareStatement("SELECT clanId FROM characters WHERE charId=?")) { ps.setInt(1, objid); byte answer = 0; try (var rs = ps.executeQuery()) { int clanId = rs.next() ? rs.getInt(1) : 0; if (clanId != 0) { L2Clan clan = ClanTable.getInstance().getClan(clanId); if (clan == null) { answer = 0; // jeezes! } else if (clan.getLeaderId() == objid) { answer = 2; } else { answer = 1; } } // Setting delete time if (answer == 0) { if (character().getDeleteCharAfterDays() == 0) { deleteCharByObjId(objid); } else { try (var ps2 = con.prepareStatement("UPDATE characters SET deletetime=? WHERE charId=?")) { ps2.setLong(1, System.currentTimeMillis() + DAYS.toMillis(character().getDeleteCharAfterDays())); ps2.setInt(2, objid); ps2.execute(); } } LOG_ACCOUNTING.info("Deleted character Id {} and client {}.", objid, this); } } return answer; } catch (Exception ex) { LOG.error("There has been an error updating delete time of character!", ex); return -1; } } /** * Save the L2PcInstance to the database. */ public void saveCharToDisk() { try { if (getActiveChar() != null) { getActiveChar().storeMe(); getActiveChar().getRecSystem().store(); if (general().updateItemsOnCharStore()) { getActiveChar().getInventory().updateDatabase(); getActiveChar().getWarehouse().updateDatabase(); } } } catch (Exception ex) { LOG.error("There has been an error saving character!", ex); } } public void markRestoredChar(int charslot) { final int objid = getObjectIdForSlot(charslot); if (objid < 0) { return; } try (var con = ConnectionFactory.getInstance().getConnection(); var ps = con.prepareStatement("UPDATE characters SET deletetime=0 WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } catch (Exception ex) { LOG.error("There has been an error restoring character!", ex); } LOG_ACCOUNTING.info("Restoring Character Id {}, client {}.", objid, this); } public static void deleteCharByObjId(int objid) { if (objid < 0) { return; } CharNameTable.getInstance().removeName(objid); try (var con = ConnectionFactory.getInstance().getConnection()) { try (var ps = con.prepareStatement("DELETE FROM character_contacts WHERE charId=? OR contactId=?")) { ps.setInt(1, objid); ps.setInt(2, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_friends WHERE charId=? OR friendId=?")) { ps.setInt(1, objid); ps.setInt(2, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_hennas WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_macroses WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_quests WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_quest_global_data WHERE charId=?")) { ps.setInt(1, objid); ps.executeUpdate(); } try (var ps = con.prepareStatement("DELETE FROM character_recipebook WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_shortcuts WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_skills WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_skills_save WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_subclasses WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM heroes WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM olympiad_nobles WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM seven_signs WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM pets WHERE item_obj_id IN (SELECT object_id FROM items WHERE items.owner_id=?)")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM item_attributes WHERE itemId IN (SELECT object_id FROM items WHERE items.owner_id=?)")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM items WHERE owner_id=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM merchant_lease WHERE player_id=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_raid_points WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_reco_bonus WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_instance_time WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM character_variables WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } try (var ps = con.prepareStatement("DELETE FROM characters WHERE charId=?")) { ps.setInt(1, objid); ps.execute(); } if (customs().allowWedding()) { try (var ps = con.prepareStatement("DELETE FROM mods_wedding WHERE player1Id = ? OR player2Id = ?")) { ps.setInt(1, objid); ps.setInt(2, objid); ps.execute(); } } } catch (Exception ex) { LOG.warn("There has been an error deleting character Id {}!", objid, ex); } } public L2PcInstance loadCharFromDisk(int charslot) { final int objId = getObjectIdForSlot(charslot); if (objId < 0) { return null; } L2PcInstance character = L2World.getInstance().getPlayer(objId); if (character != null) { // exploit prevention, should not happens in normal way LOG.error("Attempt of double login {}, account {}!", character, getAccountName()); if (character.getClient() != null) { character.getClient().closeNow(); } else { character.deleteMe(); } return null; } character = L2PcInstance.load(objId); if (character != null) { character.setRunning(); character.standUp(); character.refreshOverloaded(); character.refreshExpertisePenalty(); character.setOnlineStatus(true, false); } else { LOG.error("Could not restore in slot {}!", charslot); } return character; } public void setCharSelection(List<CharSelectInfoPackage> list) { _charSlotMapping = list; } public void setCharSelectionSlot(int charSlot) { _charSlot = charSlot; } public CharSelectInfoPackage getCharSelection() { return getCharSelection(_charSlot); } public CharSelectInfoPackage getCharSelection(int charslot) { if ((_charSlotMapping == null) || (charslot < 0) || (charslot >= _charSlotMapping.size())) { return null; } return _charSlotMapping.get(charslot); } public SecondaryPasswordAuth getSecondaryAuth() { return _secondaryAuth; } public void close(L2GameServerPacket gsp) { if (getConnection() == null) { return; // offline shop } if (_additionalClosePacket != null) { getConnection().close(new L2GameServerPacket[] { _additionalClosePacket, gsp }); } else { getConnection().close(gsp); } } public void close(L2GameServerPacket[] gspArray) { if (getConnection() == null) { return; // offline shop } getConnection().close(gspArray); } private int getObjectIdForSlot(int charSlot) { final CharSelectInfoPackage info = getCharSelection(charSlot); if (info == null) { LOG.warn("{} tried to delete character in slot {} but no characters exits at that slot!", this, charSlot); return -1; } return info.getObjectId(); } @Override protected void onForcedDisconnection() { LOG_ACCOUNTING.warn("Client {} disconnected abnormally!", this); } @Override protected void onDisconnection() { // no long running tasks here, do it async try { ThreadPoolManager.getInstance().executeGeneral(new DisconnectTask(this)); } catch (RejectedExecutionException e) { // server is closing } } /** * Close client connection with {@link ServerClose} packet */ public void closeNow() { _isDetached = true; // prevents more packets execution close(ServerClose.STATIC_PACKET); synchronized (this) { if (_cleanupTask != null) { cancelCleanup(); } _cleanupTask = ThreadPoolManager.getInstance().scheduleGeneral(new CleanupTask(), 0); // instant } } /** * Produces the best possible string representation of this client. */ @Override public String toString() { try { final InetAddress address = getConnection().getInetAddress(); return switch (getState()) { case CONNECTED -> "[IP: " + (address == null ? "disconnected" : address.getHostAddress()) + "]"; case AUTHED -> "[Account: " + getAccountName() + " - IP: " + (address == null ? "disconnected" : address.getHostAddress()) + "]"; case JOINING, IN_GAME -> "[Character: " + (getActiveChar() == null ? "disconnected" : getActiveChar().getName() + "[" + getActiveChar().getObjectId() + "]") + " - Account: " + getAccountName() + " - IP: " + (address == null ? "disconnected" : address.getHostAddress()) + "]"; }; } catch (NullPointerException e) { return "[Character read failed due to disconnect]"; } } protected class DisconnectTask implements Runnable { private final L2GameClient client; public DisconnectTask(L2GameClient client) { this.client = client; } @Override public void run() { boolean fast = true; try { final L2PcInstance player = client.getActiveChar(); if ((player != null) && !isDetached()) { setDetached(true); if (offlineMode(player)) { player.leaveParty(); OlympiadManager.getInstance().unRegisterNoble(player); // If the L2PcInstance has Pet, unsummon it if (player.hasSummon()) { player.getSummon().setRestoreSummon(true); player.getSummon().unSummon(player); // Dead pet wasn't unsummoned, broadcast npcinfo changes (pet will be without owner name - means owner offline) if (player.getSummon() != null) { player.getSummon().broadcastNpcInfo(0); } } if (customs().offlineSetNameColor()) { player.getAppearance().setNameColor(customs().getOfflineNameColor()); player.broadcastUserInfo(); } if (player.getOfflineStartTime() == 0) { player.setOfflineStartTime(System.currentTimeMillis()); } LOG_ACCOUNTING.info("Client {} entering offline mode.", client); return; } fast = !player.isInCombat() && !player.isLocked(); } cleanMe(fast); } catch (Exception ex) { LOG.warn("There has been an error while disconnecting client {}!", client, ex); } } } /** * @param player the player to be check. * @return {@code true} if the player is allowed to remain as off-line shop. */ protected boolean offlineMode(L2PcInstance player) { if (player.isInOlympiadMode() || player.isFestivalParticipant() || player.isBlockedFromExit() || player.isJailed() || (player.getVehicle() != null)) { return false; } boolean canSetShop = switch (player.getPrivateStoreType()) { case SELL, PACKAGE_SELL, BUY -> customs().offlineTradeEnable(); case MANUFACTURE -> customs().offlineTradeEnable(); default -> customs().offlineCraftEnable() && player.isInCraftMode(); }; if (customs().offlineModeInPeaceZone() && !player.isInsideZone(ZoneId.PEACE)) { canSetShop = false; } return canSetShop; } public void cleanMe(boolean fast) { try { synchronized (this) { if (_cleanupTask == null) { _cleanupTask = ThreadPoolManager.getInstance().scheduleGeneral(new CleanupTask(), fast ? 5 : 15000L); } } } catch (Exception ex) { LOG.warn("There has been an error during cleanup!", ex); } } protected class CleanupTask implements Runnable { @Override public void run() { try { // we are going to manually save the char bellow thus we can force the cancel if (_autoSaveInDB != null) { _autoSaveInDB.cancel(true); // ThreadPoolManager.getInstance().removeGeneral((Runnable) _autoSaveInDB); } // this should only happen on connection loss if (getActiveChar() != null) { if (getActiveChar().isLocked()) { LOG.warn("Player {} still performing subclass actions during disconnect!", getActiveChar()); } // we store all data from players who are disconnected while in an event in order to restore it in the next login if (L2Event.isParticipant(getActiveChar())) { L2Event.savePlayerEventStatus(getActiveChar()); } // prevent closing again getActiveChar().setClient(null); if (getActiveChar().isOnline()) { getActiveChar().deleteMe(); AntiFeedManager.getInstance().onDisconnect(L2GameClient.this); } } setActiveChar(null); } catch (Exception ex) { LOG.warn("There has been an error while cleanup client!", ex); } finally { LoginServerThread.getInstance().sendLogout(getAccountName()); } } } protected class AutoSaveTask implements Runnable { @Override public void run() { try { final var player = getActiveChar(); // Safety precaution. if ((player != null) && player.isOnline()) { saveCharToDisk(); if (player.hasSummon()) { player.getSummon().storeMe(); } } } catch (Exception ex) { LOG.error("There has been an error on AutoSaveTask!", ex); } } } public boolean isProtocolOk() { return _protocol; } public void setProtocolOk(boolean b) { _protocol = b; } public boolean handleCheat(String punishment) { if (_activeChar != null) { Util.handleIllegalPlayerAction(_activeChar, toString() + ": " + punishment); return true; } LOG_AUDIT.warn("Client {} kicked for {}!", this, punishment); closeNow(); return false; } /** * True if detached, or flood detected, or queue overflow detected and queue still not empty. * @return false if client can receive packets. */ public boolean dropPacket() { if (_isDetached) { return true; } // Flood protection. if (getStats().countPacket(_packetQueue.size())) { sendPacket(ActionFailed.STATIC_PACKET); return true; } return getStats().dropPacket(); } /** * Counts buffer underflow exceptions. */ public void onBufferUnderflow() { if (getStats().countUnderflowException()) { LOG.error("Client {} disconnected, too many buffer underflow exceptions!", this); closeNow(); return; } // In CONNECTED state kick client immediately. if (_state == GameClientState.CONNECTED) { if (general().packetHandlerDebug()) { LOG.error("Client {} disconnected, too many buffer underflow in non-authed state!", this); } closeNow(); } } /** * Counts unknown packets */ public void onUnknownPacket() { if (getStats().countUnknownPacket()) { LOG.error("Client {} disconnected, too many unknown packets!", this); closeNow(); return; } // In CONNECTED state kick client immediately. if (_state == GameClientState.CONNECTED) { if (general().packetHandlerDebug()) { LOG.error("Client {} disconnected, too many unknown packets in non-authed state!", this); } closeNow(); } } /** * Add packet to the queue and start worker thread if needed * @param packet */ public void execute(ReceivablePacket<L2GameClient> packet) { if (getStats().countFloods()) { LOG.error("Client {} disconnected, too many floods {} long and {} short!", this, getStats().longFloods, getStats().shortFloods); closeNow(); return; } if (!_packetQueue.offer(packet)) { if (getStats().countQueueOverflow()) { LOG.error("Client {} disconnected, too many queue overflows!", this); closeNow(); } else { sendPacket(ActionFailed.STATIC_PACKET); } return; } if (_queueLock.isLocked()) { return; } try { if (_state == GameClientState.CONNECTED) { if (getStats().processedPackets > 3) { if (general().packetHandlerDebug()) { LOG.error("Client {} disconnected, too many packets in non-authed state!", this); } closeNow(); return; } ThreadPoolManager.getInstance().executeIOPacket(this); } else { ThreadPoolManager.getInstance().executePacket(this); } } catch (RejectedExecutionException ex) { // If the server is shutdown we ignore. if (!ThreadPoolManager.getInstance().isShutdown()) { LOG.error("Failed executing {}, for client {}!", packet.getClass().getSimpleName(), toString(), ex); } } } @Override public void run() { if (!_queueLock.tryLock()) { return; } try { int count = 0; ReceivablePacket<L2GameClient> packet; while (true) { packet = _packetQueue.poll(); if (packet == null) { return; } // Clear queue immediately after detach. if (_isDetached) { _packetQueue.clear(); return; } try { packet.run(); } catch (Exception ex) { LOG.error("Exception during execution {}, client {}!", packet.getClass().getSimpleName(), this, ex); } count++; if (getStats().countBurst(count)) { return; } } } finally { _queueLock.unlock(); } } public void setClientTracert(int[][] tracert) { trace = tracert; } public int[][] getTrace() { return trace; } private boolean cancelCleanup() { final Future<?> task = _cleanupTask; if (task != null) { _cleanupTask = null; return task.cancel(true); } return false; } public void setAdditionalClosePacket(L2GameServerPacket additionalClosePacket) { _additionalClosePacket = additionalClosePacket; } }