Handle disconnect solely in TransitionService

This commit is contained in:
P0nk
2024-09-13 22:59:55 +02:00
parent 719b079cbc
commit f41268cdde
14 changed files with 85 additions and 275 deletions

View File

@@ -1755,32 +1755,31 @@ public class Character extends AbstractCharacterObject {
sendPacket(warpPacket);
map.removePlayer(this);
if (client.getChannelServer().getPlayerStorage().getCharacterById(getId()) != null) {
map = to;
setPosition(pos);
map.addPlayer(this);
visitMap(map);
prtLock.lock();
try {
if (party != null) {
mpc.setMapId(to.getId());
sendPacket(PacketCreator.updateParty(client.getChannel(), party, PartyOperation.SILENT_UPDATE, null));
updatePartyMemberHPInternal();
}
} finally {
prtLock.unlock();
}
if (Character.this.getParty() != null) {
Character.this.getParty().setEnemy(k);
}
silentPartyUpdateInternal(getParty()); // EIM script calls inside
} else {
if (client.getChannelServer().getPlayerStorage().getCharacterById(id) == null) {
log.warn("Chr {} got stuck when moving to map {}", getName(), map.getId());
client.disconnect(true, false); // thanks BHB for noticing a player storage stuck case here
return;
}
map = to;
setPosition(pos);
map.addPlayer(this);
visitMap(map);
prtLock.lock();
try {
if (party != null) {
mpc.setMapId(to.getId());
sendPacket(PacketCreator.updateParty(client.getChannel(), party, PartyOperation.SILENT_UPDATE, null));
updatePartyMemberHPInternal();
}
} finally {
prtLock.unlock();
}
if (Character.this.getParty() != null) {
Character.this.getParty().setEnemy(k);
}
silentPartyUpdateInternal(getParty()); // EIM script calls inside
notifyMapTransferToPartner(map.getId());
//alas, new map has been specified when a warping was being processed...
@@ -8175,20 +8174,14 @@ public class Character extends AbstractCharacterObject {
return false;
}
// TODO: all callers should use CharacterSaver instead.
// It's supposed to act as a proxy to these 2 methods (as a first step towards taking full ownership of character saving)
public void saveCharToDB() {
saveCharToDB(true);
}
//ItemFactory saveItems and monsterbook.saveCards are the most time consuming here.
public synchronized void saveCharToDB(boolean notAutosave) {
public synchronized void saveCharToDB() {
if (!loggedIn) {
return;
}
Calendar c = Calendar.getInstance();
log.debug("Attempting to {} chr {}", notAutosave ? "save" : "autosave", name);
log.debug("Saving chr {}", name);
Server.getInstance().updateCharacterEntry(this);

View File

@@ -22,7 +22,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package client;
import config.YamlConfig;
import constants.id.MapId;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
@@ -41,34 +40,24 @@ import net.server.coordinator.login.LoginBypassCoordinator;
import net.server.coordinator.session.Hwid;
import net.server.coordinator.session.SessionCoordinator;
import net.server.coordinator.session.SessionCoordinator.AntiMulticlientResult;
import net.server.guild.Guild;
import net.server.guild.GuildCharacter;
import net.server.guild.GuildPackets;
import net.server.world.MessengerCharacter;
import net.server.world.Party;
import net.server.world.PartyCharacter;
import net.server.world.PartyOperation;
import net.server.world.World;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scripting.AbstractPlayerInteraction;
import scripting.event.EventInstanceManager;
import scripting.event.EventManager;
import scripting.npc.NPCConversationManager;
import scripting.npc.NPCScriptManager;
import scripting.quest.QuestActionManager;
import scripting.quest.QuestScriptManager;
import server.ThreadManager;
import server.TimerManager;
import server.life.Monster;
import server.maps.MapleMap;
import tools.BCrypt;
import tools.DatabaseConnection;
import tools.HexTool;
import tools.PacketCreator;
import javax.script.ScriptEngine;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -212,7 +201,8 @@ public class Client extends ChannelInboundHandlerAdapter {
MonitoredChrLogger.logPacketIfMonitored(this, opcode, packet.getBytes());
handler.handlePacket(packet, this);
} catch (GameViolationException gve) {
throw new DisconnectException(this, gve.getMessage());
log.warn("Game violation (disconnecting): {}", gve.getMessage());
throw new DisconnectException(this, true);
} catch (final Throwable t) {
final String chrInfo = player != null ? player.getName() + " on map " + player.getMapId() : "?";
log.warn("Error in packet handler {}. Chr {}, account {}. Packet: {}", handler.getClass().getSimpleName(),
@@ -232,15 +222,13 @@ public class Client extends ChannelInboundHandlerAdapter {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (player != null) {
log.warn("Exception caught by {}", player, cause);
}
if (cause instanceof InvalidPacketHeaderException) {
SessionCoordinator.getInstance().closeSession(this, true);
} else if (cause instanceof IOException) {
closeMapleSession();
} else {
ctx.fireExceptionCaught(cause);
}
@@ -248,10 +236,6 @@ public class Client extends ChannelInboundHandlerAdapter {
@Override
public void channelInactive(ChannelHandlerContext ctx) {
closeMapleSession();
}
private void closeMapleSession() {
switch (type) {
case LOGIN -> SessionCoordinator.getInstance().closeLoginSession(this);
case CHANNEL -> SessionCoordinator.getInstance().closeSession(this, false);
@@ -260,7 +244,7 @@ public class Client extends ChannelInboundHandlerAdapter {
try {
// client freeze issues on session transition states found thanks to yolinlin, Omo Oppa, Nozphex
if (!inTransition) {
disconnect(false, false);
ctx.fireExceptionCaught(new DisconnectException(this, false));
}
} catch (Throwable t) {
log.warn("Account stuck", t);
@@ -693,11 +677,6 @@ public class Client extends ChannelInboundHandlerAdapter {
}
public void updateLoginState(int newState) {
// rules out possibility of multiple account entries
if (newState == LOGIN_LOGGEDIN) {
SessionCoordinator.getInstance().updateOnlineClient(this);
}
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("UPDATE accounts SET loggedin = ?, lastlogin = ? WHERE id = ?")) {
// using sql currenttime here could potentially break the login, thanks Arnah for pointing this out
@@ -770,85 +749,6 @@ public class Client extends ChannelInboundHandlerAdapter {
return date.get(Calendar.YEAR) == birthday.get(Calendar.YEAR) && date.get(Calendar.MONTH) == birthday.get(Calendar.MONTH) && date.get(Calendar.DAY_OF_MONTH) == birthday.get(Calendar.DAY_OF_MONTH);
}
private void removePartyPlayer(World wserv) {
MapleMap map = player.getMap();
final Party party = player.getParty();
final int idz = player.getId();
if (party != null) {
final PartyCharacter chrp = new PartyCharacter(player);
chrp.setOnline(false);
wserv.updateParty(party.getId(), PartyOperation.LOG_ONOFF, chrp);
if (party.getLeader().getId() == idz && map != null) {
PartyCharacter lchr = null;
for (PartyCharacter pchr : party.getMembers()) {
if (pchr != null && pchr.getId() != idz && (lchr == null || lchr.getLevel() <= pchr.getLevel()) && map.getCharacterById(pchr.getId()) != null) {
lchr = pchr;
}
}
if (lchr != null) {
wserv.updateParty(party.getId(), PartyOperation.CHANGE_LEADER, lchr);
}
}
}
}
private void removePlayer(World wserv, boolean serverTransition) {
try {
player.setDisconnectedFromChannelWorld();
player.notifyMapTransferToPartner(-1);
player.removeIncomingInvites();
player.cancelAllBuffs(true);
player.closePlayerInteractions();
player.closePartySearchInteractions();
if (!serverTransition) { // thanks MedicOP for detecting an issue with party leader change on changing channels
removePartyPlayer(wserv);
EventInstanceManager eim = player.getEventInstance();
if (eim != null) {
eim.playerDisconnected(player);
}
if (player.getMonsterCarnival() != null) {
player.getMonsterCarnival().playerDisconnected(getPlayer().getId());
}
if (player.getAriantColiseum() != null) {
player.getAriantColiseum().playerDisconnected(getPlayer());
}
}
if (player.getMap() != null) {
int mapId = player.getMapId();
player.getMap().removePlayer(player);
if (MapId.isDojo(mapId)) {
this.getChannelServer().freeDojoSectionIfEmpty(mapId);
}
if (player.getMap().getHPDec() > 0) {
getWorldServer().removePlayerHpDecrease(player);
}
}
} catch (final Throwable t) {
log.error("Account stuck", t);
}
}
public final void disconnect(final boolean shutdown, final boolean cashshop) {
if (tryDisconnect()) {
ThreadManager.getInstance().newTask(() -> disconnectInternal(shutdown, cashshop));
}
}
public final void forceDisconnect() {
if (tryDisconnect()) {
disconnectInternal(true, false);
}
}
public synchronized boolean tryDisconnect() {
if (disconnecting) {
return false;
@@ -858,88 +758,6 @@ public class Client extends ChannelInboundHandlerAdapter {
return true;
}
private void disconnectInternal(boolean shutdown, boolean cashshop) {//once per Client instance
if (player != null && player.isLoggedin() && player.getClient() != null) {
final int messengerid = player.getMessenger() == null ? 0 : player.getMessenger().getId();
final BuddyList bl = player.getBuddylist();
final MessengerCharacter messengerChr = new MessengerCharacter(player, 0);
final GuildCharacter guildChr = player.getMGC();
final Guild guild = player.getGuild();
player.cancelMagicDoor();
final World wserv = getWorldServer(); // obviously wserv is NOT null if this player was online on it
try {
removePlayer(wserv, this.serverTransition);
if (!(channel == -1 || shutdown)) {
if (!cashshop) {
if (!this.serverTransition) { // meaning not changing channels
if (messengerid > 0) {
wserv.leaveMessenger(messengerid, messengerChr);
}
player.forfeitExpirableQuests(); //This is for those quests that you have to stay logged in for a certain amount of time
if (guild != null) {
final Server server = Server.getInstance();
server.setGuildMemberOnline(player, false, player.getClient().getChannel());
player.sendPacket(GuildPackets.showGuildInfo(player));
}
if (bl != null) {
wserv.loggedOff(player.getName(), player.getId(), channel, player.getBuddylist().getBuddyIds());
}
}
} else {
if (!this.serverTransition) { // if dc inside of cash shop.
if (bl != null) {
wserv.loggedOff(player.getName(), player.getId(), channel, player.getBuddylist().getBuddyIds());
}
}
}
}
} catch (final Exception e) {
log.error("Account stuck", e);
} finally {
if (!this.serverTransition) {
if (guildChr != null) {
guildChr.setCharacter(null);
}
wserv.removePlayer(player);
//getChannelServer().removePlayer(player); already being done
player.cancelAllDebuffs();
player.saveCharToDB();
player.logOff();
if (YamlConfig.config.server.INSTANT_NAME_CHANGE) {
player.doPendingNameChange();
}
clear();
} else {
getChannelServer().removePlayer(player);
player.cancelAllDebuffs();
player.saveCharToDB();
}
}
}
SessionCoordinator.getInstance().closeSession(this, false);
if (!serverTransition && isLoggedIn()) {
updateLoginState(Client.LOGIN_NOTLOGGEDIN);
clear();
} else {
if (!Server.getInstance().hasCharacteridInTransition(this)) {
updateLoginState(Client.LOGIN_NOTLOGGEDIN);
}
engines = null; // thanks Tochi for pointing out a NPE here
}
}
public void clear() {
// player hard reference removal thanks to Steve (kaito1410)
if (this.player != null) {
@@ -956,6 +774,12 @@ public class Client extends ChannelInboundHandlerAdapter {
this.player = null;
}
public void clearEngines() {
if (engines != null) {
engines.clear();
}
}
public void setCharacterOnSessionTransitionState(int cid) {
this.updateLoginState(Client.LOGIN_SERVER_TRANSITION);
this.inTransition = true;

View File

@@ -49,7 +49,7 @@ public class DcCommand extends Command {
victim = player.getMap().getCharacterByName(chrName);
if (victim != null) {
try {//sometimes bugged because the map = null
ctx.transitionService().disconnect(victim.getClient(), true, false);
ctx.transitionService().disconnect(victim.getClient(), true);
player.getMap().removePlayer(victim);
} catch (Exception e) {
e.printStackTrace();
@@ -62,6 +62,6 @@ public class DcCommand extends Command {
if (player.gmLevel() < victim.gmLevel()) {
victim = player;
}
ctx.transitionService().disconnect(victim.getClient(), false, false);
ctx.transitionService().disconnect(victim.getClient(), false);
}
}

View File

@@ -78,7 +78,7 @@ public class BanCommand extends Command {
c.sendPacket(PacketCreator.getGMEffect(4, (byte) 0));
final Character rip = target;
TimerManager.getInstance().schedule(
() -> ctx.transitionService().disconnect(rip.getClient(), false, false),
() -> ctx.transitionService().disconnect(rip.getClient(), false),
TimeUnit.SECONDS.toMillis(5)
);
Server.getInstance().broadcastMessage(c.getWorld(), PacketCreator.serverNotice(6, "[RIP]: " + ign + " has been banned."));

View File

@@ -41,7 +41,7 @@ public class DCAllCommand extends Command {
for (World world : Server.getInstance().getWorlds()) {
for (Character chr : world.getPlayerStorage().getAllCharacters()) {
if (!chr.isGM()) {
ctx.transitionService().disconnect(chr.getClient(), false, false);
ctx.transitionService().disconnect(chr.getClient(), false);
}
}
}

View File

@@ -11,7 +11,7 @@ public class CharacterSaver {
}
public void save(Character chr) {
chr.saveCharToDB(false);
chr.saveCharToDB();
// Saving monster cards to both MySQL and Postgres for now
monsterCardDao.save(chr.getId(), chr.getMonsterBook().getCards());

View File

@@ -4,13 +4,18 @@ import client.Client;
public class DisconnectException extends RuntimeException {
private final Client client;
private final boolean shutdown;
public DisconnectException(Client client, String message) {
super(message);
public DisconnectException(Client client, boolean shutdown) {
this.client = client;
this.shutdown = shutdown;
}
public Client getClient() {
return client;
}
public boolean isShutdown() {
return shutdown;
}
}

View File

@@ -17,7 +17,7 @@ public class DisconnectHandler extends ChannelInboundHandlerAdapter {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof DisconnectException de) {
var client = de.getClient();
transitionService.disconnect(client, true, false);
transitionService.disconnect(client, true);
} else {
ctx.fireExceptionCaught(cause);
}

View File

@@ -1923,7 +1923,7 @@ public class Server {
for (Client c : toDisconnect) { // thanks Lei for pointing a deadlock issue with srvLock
if (c.isLoggedIn()) {
channelDependencies.transitionService().disconnect(c, false, false);
channelDependencies.transitionService().disconnect(c, false);
} else {
SessionCoordinator.getInstance().closeSession(c, true);
}

View File

@@ -195,7 +195,7 @@ public final class AdminCommandHandler extends AbstractPacketHandler {
private void sendPolice(Client c, String reason) {
c.sendPacket(PacketCreator.sendPolice(String.format("You have been blocked by the#b %s Police for %s.#k", "Cosmic", reason)));
c.getPlayer().setBanned();
TimerManager.getInstance().schedule(() -> transitionService.disconnect(c, false, false),
TimerManager.getInstance().schedule(() -> transitionService.disconnect(c, false),
TimeUnit.SECONDS.toMillis(6));
}
}

View File

@@ -109,24 +109,6 @@ public class SessionCoordinator {
}
}
/**
* Overwrites any existing online client for the account id, making sure to disconnect it as well.
*/
public void updateOnlineClient(Client client) {
if (client != null) {
int accountId = client.getAccID();
disconnectClientIfOnline(accountId);
onlineClients.put(accountId, client);
}
}
private void disconnectClientIfOnline(int accountId) {
Client ingameClient = onlineClients.get(accountId);
if (ingameClient != null) { // thanks MedicOP for finding out a loss of loggedin account uniqueness when using the CMS "Unstuck" feature
ingameClient.forceDisconnect();
}
}
public boolean canStartLoginSession(Client client) {
if (!YamlConfig.config.server.DETERRED_MULTICLIENT) {
return true;

View File

@@ -28,7 +28,7 @@ public class TimeoutTask extends BaseTask implements Runnable {
for (Character chr : chars) {
if (time - chr.getClient().getLastPacket() > YamlConfig.config.server.TIMEOUT_DURATION) {
log.info("Chr {} auto-disconnected due to inactivity", chr.getName());
transitionService.disconnect(chr.getClient(), true, chr.getCashShop().isOpened());
transitionService.disconnect(chr.getClient(), true);
}
}
}

View File

@@ -32,7 +32,7 @@ public class BanService {
chr.ban(reason);
chr.sendPacket(PacketCreator.sendPolice("You have been blocked by the#b %s Police for HACK reason.#k".formatted("Cosmic")));
TimerManager.getInstance().schedule(() -> transitionService.disconnect(chr.getClient(), false, false),
TimerManager.getInstance().schedule(() -> transitionService.disconnect(chr.getClient(), false),
TimeUnit.SECONDS.toMillis(5));
var bannedName = Character.makeMapleReadable(chr.getName());

View File

@@ -6,7 +6,9 @@ import client.inventory.InventoryType;
import config.YamlConfig;
import constants.id.MapId;
import database.character.CharacterSaver;
import net.netty.LoginServer;
import net.server.Server;
import net.server.coordinator.session.SessionCoordinator;
import net.server.guild.Guild;
import net.server.guild.GuildCharacter;
import net.server.guild.GuildPackets;
@@ -39,7 +41,7 @@ public class TransitionService {
public void changeChannel(Client c, int channel) {
var chr = c.getPlayer();
if (chr.isBanned()) {
disconnect(c, false, false);
disconnect(c, false);
return;
}
@@ -93,21 +95,19 @@ public class TransitionService {
}
}
// TODO: take code from Client#disconnect & forceDisconnect. Move it here.
// It's not gonna be easy to move all instances of c.disconnect, but it has to be done.
public void disconnect(final Client c, final boolean shutdown, final boolean cashShop) {
public void disconnect(final Client c, final boolean shutdown) {
if (c.tryDisconnect()) {
ThreadManager.getInstance().newTask(() -> disconnectInternal(c, shutdown, cashShop));
ThreadManager.getInstance().newTask(() -> disconnectInternal(c, shutdown));
}
}
public void forceDisconnect(Client c) {
if (c.tryDisconnect()) {
disconnectInternal(c, true, false);
disconnectInternal(c, true);
}
}
private void disconnectInternal(Client c, boolean shutdown, boolean cashShop) {
private void disconnectInternal(Client c, boolean shutdown) {
var chr = c.getPlayer();
if (chr != null && chr.isLoggedin() && chr.getClient() != null) {
final int messengerid = chr.getMessenger() == null ? 0 : chr.getMessenger().getId();
@@ -123,29 +123,21 @@ public class TransitionService {
removePlayer(c, wserv, c.isInTransition());
final int channel = c.getChannel();
if (!(channel == -1 || shutdown)) {
if (!cashShop) {
if (!c.isInTransition()) { // meaning not changing channels
if (messengerid > 0) {
wserv.leaveMessenger(messengerid, messengerChr);
}
chr.forfeitExpirableQuests(); //This is for those quests that you have to stay logged in for a certain amount of time
if (guild != null) {
final Server server = Server.getInstance();
server.setGuildMemberOnline(chr, false, chr.getClient().getChannel());
chr.sendPacket(GuildPackets.showGuildInfo(chr));
}
if (bl != null) {
wserv.loggedOff(chr.getName(), chr.getId(), channel, chr.getBuddylist().getBuddyIds());
}
if (!(channel == LoginServer.CHANNEL_ID || shutdown)) {
if (!c.isInTransition()) { // meaning not changing channels
if (messengerid > 0) {
wserv.leaveMessenger(messengerid, messengerChr);
}
} else {
if (!c.isInTransition()) { // if dc inside of cash shop.
if (bl != null) {
wserv.loggedOff(chr.getName(), chr.getId(), channel, chr.getBuddylist().getBuddyIds());
}
chr.forfeitExpirableQuests(); //This is for those quests that you have to stay logged in for a certain amount of time
if (guild != null) {
final Server server = Server.getInstance();
server.setGuildMemberOnline(chr, false, chr.getClient().getChannel());
chr.sendPacket(GuildPackets.showGuildInfo(chr));
}
if (bl != null) {
wserv.loggedOff(chr.getName(), chr.getId(), channel, chr.getBuddylist().getBuddyIds());
}
}
}
@@ -175,6 +167,20 @@ public class TransitionService {
}
}
}
SessionCoordinator.getInstance().closeSession(c, false);
if (!c.isInTransition() && c.isLoggedIn()) {
c.updateLoginState(Client.LOGIN_NOTLOGGEDIN);
c.clear();
} else {
if (!Server.getInstance().hasCharacteridInTransition(c)) {
c.updateLoginState(Client.LOGIN_NOTLOGGEDIN);
}
c.clearEngines();
}
}
private void removePlayer(Client c, World world, boolean serverTransition) {