Add client disconnection logic to TransitionService

Problem: disconnecting requires access to CharacterSaver,
which is not available in Client.
Having it in a service like this solves that problem.

Next step is to migrate all calls to Client#disconnect and Client#forceDisconnect
to their TransitionService counterparts.
This commit is contained in:
P0nk
2023-08-08 21:51:12 +02:00
parent f6d06ba82a
commit d5682a5f65
10 changed files with 272 additions and 102 deletions

View File

@@ -305,6 +305,10 @@ public class Client extends ChannelInboundHandlerAdapter {
return loggedIn; return loggedIn;
} }
public boolean isInTransition() {
return serverTransition;
}
public boolean hasBannedIP() { public boolean hasBannedIP() {
boolean ret = false; boolean ret = false;
try (Connection con = DatabaseConnection.getConnection(); try (Connection con = DatabaseConnection.getConnection();
@@ -833,7 +837,7 @@ public class Client extends ChannelInboundHandlerAdapter {
} }
} }
private synchronized boolean tryDisconnect() { public synchronized boolean tryDisconnect() {
if (disconnecting) { if (disconnecting) {
return false; return false;
} }
@@ -924,7 +928,7 @@ public class Client extends ChannelInboundHandlerAdapter {
} }
} }
private void clear() { public void clear() {
// player hard reference removal thanks to Steve (kaito1410) // player hard reference removal thanks to Steve (kaito1410)
if (this.player != null) { if (this.player != null) {
this.player.empty(true); // clears schedules and stuff this.player.empty(true); // clears schedules and stuff

View File

@@ -7,6 +7,7 @@ package client.autoban;
import client.Character; import client.Character;
import config.YamlConfig; import config.YamlConfig;
import net.netty.GameViolationException;
import net.server.Server; import net.server.Server;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -118,11 +119,10 @@ public class AutobanManager {
if (this.timestamp[type] == time) { if (this.timestamp[type] == time) {
this.timestampcounter[type]++; this.timestampcounter[type]++;
if (this.timestampcounter[type] >= times) { if (this.timestampcounter[type] >= times) {
if (YamlConfig.config.server.USE_AUTOBAN) {
chr.getClient().disconnect(false, false);
}
log.info("Autoban - Chr {} was caught spamming TYPE {} and has been disconnected", chr, type); log.info("Autoban - Chr {} was caught spamming TYPE {} and has been disconnected", chr, type);
if (YamlConfig.config.server.USE_AUTOBAN) {
throw new GameViolationException("Auto ban");
}
} }
} else { } else {
this.timestamp[type] = time; this.timestamp[type] = time;

View File

@@ -3,15 +3,15 @@ package client.command;
import database.character.CharacterSaver; import database.character.CharacterSaver;
import database.drop.DropProvider; import database.drop.DropProvider;
import server.shop.ShopFactory; import server.shop.ShopFactory;
import service.ChannelService; import service.TransitionService;
/** /**
* @author Ponk * @author Ponk
*/ */
public record CommandContext(CommandsExecutor commandsExecutor, DropProvider dropProvider, ShopFactory shopFactory, public record CommandContext(CommandsExecutor commandsExecutor, DropProvider dropProvider, ShopFactory shopFactory,
CharacterSaver characterSaver, ChannelService channelService) { CharacterSaver characterSaver, TransitionService transitionService) {
public CommandContext with(CommandsExecutor ce) { public CommandContext with(CommandsExecutor ce) {
return new CommandContext(ce, this.dropProvider, this.shopFactory, this.characterSaver, this.channelService); return new CommandContext(ce, this.dropProvider, this.shopFactory, this.characterSaver, this.transitionService);
} }
} }

View File

@@ -63,7 +63,7 @@ public class SummonCommand extends Command {
if (player.getClient().getChannel() != victim.getClient().getChannel()) {//And then change channel if needed. if (player.getClient().getChannel() != victim.getClient().getChannel()) {//And then change channel if needed.
victim.dropMessage("Changing channel, please wait a moment."); victim.dropMessage("Changing channel, please wait a moment.");
ctx.channelService().changeChannel(victim.getClient(), player.getClient().getChannel()); ctx.transitionService().changeChannel(victim.getClient(), player.getClient().getChannel());
} }
try { try {

View File

@@ -7,8 +7,8 @@ import database.character.CharacterLoader;
import database.character.CharacterSaver; import database.character.CharacterSaver;
import database.drop.DropProvider; import database.drop.DropProvider;
import server.shop.ShopFactory; import server.shop.ShopFactory;
import service.ChannelService;
import service.NoteService; import service.NoteService;
import service.TransitionService;
import java.util.Objects; import java.util.Objects;
@@ -18,7 +18,7 @@ import java.util.Objects;
public record ChannelDependencies( public record ChannelDependencies(
CharacterLoader characterLoader, CharacterSaver characterSaver, NoteService noteService, CharacterLoader characterLoader, CharacterSaver characterSaver, NoteService noteService,
FredrickProcessor fredrickProcessor, MakerProcessor makerProcessor, DropProvider dropProvider, FredrickProcessor fredrickProcessor, MakerProcessor makerProcessor, DropProvider dropProvider,
CommandsExecutor commandsExecutor, ShopFactory shopFactory, ChannelService channelService CommandsExecutor commandsExecutor, ShopFactory shopFactory, TransitionService transitionService
) { ) {
public ChannelDependencies { public ChannelDependencies {
@@ -30,6 +30,6 @@ public record ChannelDependencies(
Objects.requireNonNull(dropProvider); Objects.requireNonNull(dropProvider);
Objects.requireNonNull(commandsExecutor); Objects.requireNonNull(commandsExecutor);
Objects.requireNonNull(shopFactory); Objects.requireNonNull(shopFactory);
Objects.requireNonNull(channelService); Objects.requireNonNull(transitionService);
} }
} }

View File

@@ -140,7 +140,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.NAME_TRANSFER, new TransferNameHandler()); registerHandler(RecvOpcode.NAME_TRANSFER, new TransferNameHandler());
registerHandler(RecvOpcode.CHECK_CHAR_NAME, new TransferNameResultHandler()); registerHandler(RecvOpcode.CHECK_CHAR_NAME, new TransferNameResultHandler());
registerHandler(RecvOpcode.WORLD_TRANSFER, new TransferWorldHandler()); registerHandler(RecvOpcode.WORLD_TRANSFER, new TransferWorldHandler());
registerHandler(RecvOpcode.CHANGE_CHANNEL, new ChangeChannelHandler(channelDeps.channelService())); registerHandler(RecvOpcode.CHANGE_CHANNEL, new ChangeChannelHandler(channelDeps.transitionService()));
registerHandler(RecvOpcode.STRANGE_DATA, LoginRequiringNoOpHandler.getInstance()); registerHandler(RecvOpcode.STRANGE_DATA, LoginRequiringNoOpHandler.getInstance());
registerHandler(RecvOpcode.GENERAL_CHAT, new GeneralChatHandler(channelDeps.commandsExecutor())); registerHandler(RecvOpcode.GENERAL_CHAT, new GeneralChatHandler(channelDeps.commandsExecutor()));
registerHandler(RecvOpcode.WHISPER, new WhisperHandler()); registerHandler(RecvOpcode.WHISPER, new WhisperHandler());

View File

@@ -76,8 +76,8 @@ import server.expeditions.ExpeditionBossLog;
import server.life.PlayerNPCFactory; import server.life.PlayerNPCFactory;
import server.quest.Quest; import server.quest.Quest;
import server.shop.ShopFactory; import server.shop.ShopFactory;
import service.ChannelService;
import service.NoteService; import service.NoteService;
import service.TransitionService;
import tools.DatabaseConnection; import tools.DatabaseConnection;
import tools.Pair; import tools.Pair;
@@ -982,17 +982,17 @@ public class Server {
MonsterCardDao monsterCardDao = new MonsterCardDao(connection); MonsterCardDao monsterCardDao = new MonsterCardDao(connection);
CharacterLoader characterLoader = new CharacterLoader(monsterCardDao); CharacterLoader characterLoader = new CharacterLoader(monsterCardDao);
CharacterSaver characterSaver = new CharacterSaver(monsterCardDao); CharacterSaver characterSaver = new CharacterSaver(monsterCardDao);
ChannelService channelService = new ChannelService(characterSaver); TransitionService transitionService = new TransitionService(characterSaver);
NoteService noteService = new NoteService(new NoteDao(connection)); NoteService noteService = new NoteService(new NoteDao(connection));
MakerProcessor makerProcessor = new MakerProcessor(new MakerInfoProvider(new MakerDao(connection))); MakerProcessor makerProcessor = new MakerProcessor(new MakerInfoProvider(new MakerDao(connection)));
FredrickProcessor fredrickProcessor = new FredrickProcessor(noteService); FredrickProcessor fredrickProcessor = new FredrickProcessor(noteService);
DropProvider dropProvider = new DropProvider(new DropDao(connection)); DropProvider dropProvider = new DropProvider(new DropDao(connection));
ShopFactory shopFactory = new ShopFactory(new ShopDao(connection)); ShopFactory shopFactory = new ShopFactory(new ShopDao(connection));
CommandContext commandContext = new CommandContext(null, dropProvider, shopFactory, CommandContext commandContext = new CommandContext(null, dropProvider, shopFactory,
characterSaver, channelService); characterSaver, transitionService);
CommandsExecutor commandsExecutor = new CommandsExecutor(commandContext); CommandsExecutor commandsExecutor = new CommandsExecutor(commandContext);
ChannelDependencies channelDependencies = new ChannelDependencies(characterLoader, characterSaver, noteService, ChannelDependencies channelDependencies = new ChannelDependencies(characterLoader, characterSaver, noteService,
fredrickProcessor, makerProcessor, dropProvider, commandsExecutor, shopFactory, channelService); fredrickProcessor, makerProcessor, dropProvider, commandsExecutor, shopFactory, transitionService);
PacketProcessor.registerGameHandlerDependencies(channelDependencies); PacketProcessor.registerGameHandlerDependencies(channelDependencies);

View File

@@ -27,16 +27,16 @@ import net.AbstractPacketHandler;
import net.netty.GameViolationException; import net.netty.GameViolationException;
import net.packet.InPacket; import net.packet.InPacket;
import net.server.Server; import net.server.Server;
import service.ChannelService; import service.TransitionService;
/** /**
* @author Matze * @author Matze
*/ */
public final class ChangeChannelHandler extends AbstractPacketHandler { public final class ChangeChannelHandler extends AbstractPacketHandler {
private final ChannelService channelService; private final TransitionService transitionService;
public ChangeChannelHandler(ChannelService channelService) { public ChangeChannelHandler(TransitionService transitionService) {
this.channelService = channelService; this.transitionService = transitionService;
} }
@Override @Override
@@ -51,6 +51,6 @@ public final class ChangeChannelHandler extends AbstractPacketHandler {
return; return;
} }
channelService.changeChannel(c, channel); transitionService.changeChannel(c, channel);
} }
} }

View File

@@ -1,79 +0,0 @@
package service;
import client.Client;
import client.inventory.InventoryType;
import database.character.CharacterSaver;
import net.server.Server;
import server.maps.FieldLimit;
import server.maps.MiniDungeonInfo;
import tools.PacketCreator;
import java.io.IOException;
import java.net.InetAddress;
public class ChannelService {
private final Server server = Server.getInstance();
private final CharacterSaver chrSaver;
public ChannelService(CharacterSaver characterSaver) {
this.chrSaver = characterSaver;
}
public void changeChannel(Client c, int channel) {
var chr = c.getPlayer();
if (chr.isBanned()) {
c.disconnect(false, false);
return;
}
if (!chr.isAlive() || FieldLimit.CANNOTMIGRATE.check(chr.getMap().getFieldLimit())) {
c.sendPacket(PacketCreator.enableActions());
return;
}
if (MiniDungeonInfo.isDungeonMap(chr.getMapId())) {
c.sendPacket(PacketCreator.serverNotice(5, "Changing channels or entering Cash Shop or MTS are disabled when inside a Mini-Dungeon."));
c.sendPacket(PacketCreator.enableActions());
return;
}
String[] socket = Server.getInstance().getInetSocket(c, c.getWorld(), channel);
if (socket == null) {
c.sendPacket(PacketCreator.serverNotice(1, "Channel " + channel + " is currently disabled. Try another channel."));
c.sendPacket(PacketCreator.enableActions());
return;
}
chr.closePlayerInteractions();
chr.closePartySearchInteractions();
chr.unregisterChairBuff();
server.getPlayerBuffStorage().addBuffsToStorage(chr.getId(), chr.getAllBuffs());
server.getPlayerBuffStorage().addDiseasesToStorage(chr.getId(), chr.getAllDiseases());
chr.setDisconnectedFromChannelWorld();
chr.notifyMapTransferToPartner(-1);
chr.removeIncomingInvites();
chr.cancelAllBuffs(true);
chr.cancelAllDebuffs();
chr.cancelBuffExpireTask();
chr.cancelDiseaseExpireTask();
chr.cancelSkillCooldownTask();
chr.cancelQuestExpirationTask();
//Cancelling magicdoor? Nope
//Cancelling mounts? Noty
chr.getInventory(InventoryType.EQUIPPED).checked(false); //test
chr.getMap().removePlayer(chr);
chr.clearBanishPlayerData();
c.getChannelServer().removePlayer(chr);
chrSaver.save(chr);
chr.setSessionTransitionState();
try {
c.sendPacket(PacketCreator.getChannelChange(InetAddress.getByName(socket[0]), Integer.parseInt(socket[1])));
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,245 @@
package service;
import client.BuddyList;
import client.Client;
import client.inventory.InventoryType;
import config.YamlConfig;
import constants.id.MapId;
import database.character.CharacterSaver;
import net.server.Server;
import net.server.guild.Guild;
import net.server.guild.GuildCharacter;
import net.server.guild.GuildPackets;
import net.server.world.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scripting.event.EventInstanceManager;
import server.ThreadManager;
import server.maps.FieldLimit;
import server.maps.MapleMap;
import server.maps.MiniDungeonInfo;
import tools.PacketCreator;
import java.io.IOException;
import java.net.InetAddress;
public class TransitionService {
private static final Logger log = LoggerFactory.getLogger(TransitionService.class);
private final Server server = Server.getInstance();
private final CharacterSaver chrSaver;
public TransitionService(CharacterSaver characterSaver) {
this.chrSaver = characterSaver;
}
public void changeChannel(Client c, int channel) {
var chr = c.getPlayer();
if (chr.isBanned()) {
disconnect(c, false, false);
return;
}
if (!chr.isAlive() || FieldLimit.CANNOTMIGRATE.check(chr.getMap().getFieldLimit())) {
c.sendPacket(PacketCreator.enableActions());
return;
}
if (MiniDungeonInfo.isDungeonMap(chr.getMapId())) {
c.sendPacket(PacketCreator.serverNotice(5, "Changing channels or entering Cash Shop or MTS are disabled when inside a Mini-Dungeon."));
c.sendPacket(PacketCreator.enableActions());
return;
}
String[] socket = Server.getInstance().getInetSocket(c, c.getWorld(), channel);
if (socket == null) {
c.sendPacket(PacketCreator.serverNotice(1, "Channel " + channel + " is currently disabled. Try another channel."));
c.sendPacket(PacketCreator.enableActions());
return;
}
chr.closePlayerInteractions();
chr.closePartySearchInteractions();
chr.unregisterChairBuff();
server.getPlayerBuffStorage().addBuffsToStorage(chr.getId(), chr.getAllBuffs());
server.getPlayerBuffStorage().addDiseasesToStorage(chr.getId(), chr.getAllDiseases());
chr.setDisconnectedFromChannelWorld();
chr.notifyMapTransferToPartner(-1);
chr.removeIncomingInvites();
chr.cancelAllBuffs(true);
chr.cancelAllDebuffs();
chr.cancelBuffExpireTask();
chr.cancelDiseaseExpireTask();
chr.cancelSkillCooldownTask();
chr.cancelQuestExpirationTask();
//Cancelling magicdoor? Nope
//Cancelling mounts? Noty
chr.getInventory(InventoryType.EQUIPPED).checked(false); //test
chr.getMap().removePlayer(chr);
chr.clearBanishPlayerData();
c.getChannelServer().removePlayer(chr);
chrSaver.save(chr);
chr.setSessionTransitionState();
try {
c.sendPacket(PacketCreator.getChannelChange(InetAddress.getByName(socket[0]), Integer.parseInt(socket[1])));
} catch (IOException e) {
e.printStackTrace();
}
}
// 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) {
if (c.tryDisconnect()) {
ThreadManager.getInstance().newTask(() -> disconnectInternal(c, shutdown, cashShop));
}
}
public void forceDisconnect(Client c) {
if (c.tryDisconnect()) {
disconnectInternal(c, true, false);
}
}
private void disconnectInternal(Client c, boolean shutdown, boolean cashShop) {
var chr = c.getPlayer();
if (chr != null && chr.isLoggedin() && chr.getClient() != null) {
final int messengerid = chr.getMessenger() == null ? 0 : chr.getMessenger().getId();
final BuddyList bl = chr.getBuddylist();
final MessengerCharacter messengerChr = new MessengerCharacter(chr, 0);
final GuildCharacter guildChr = chr.getMGC();
final Guild guild = chr.getGuild();
chr.cancelMagicDoor();
final World wserv = c.getWorldServer(); // obviously wserv is NOT null if this chr was online on it
try {
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());
}
}
} else {
if (!c.isInTransition()) { // if dc inside of cash shop.
if (bl != null) {
wserv.loggedOff(chr.getName(), chr.getId(), channel, chr.getBuddylist().getBuddyIds());
}
}
}
}
} catch (final Exception e) {
log.error("Account stuck", e);
} finally {
if (!c.isInTransition()) {
if (guildChr != null) {
guildChr.setCharacter(null);
}
wserv.removePlayer(chr);
//getChannelServer().removePlayer(player); already being done
chr.cancelAllDebuffs();
chrSaver.save(chr);
chr.logOff();
if (YamlConfig.config.server.INSTANT_NAME_CHANGE) {
chr.doPendingNameChange();
}
c.clear();
} else {
c.getChannelServer().removePlayer(chr);
chr.cancelAllDebuffs();
chrSaver.save(chr);
}
}
}
}
private void removePlayer(Client c, World world, boolean serverTransition) {
var chr = c.getPlayer();
try {
chr.setDisconnectedFromChannelWorld();
chr.notifyMapTransferToPartner(-1);
chr.removeIncomingInvites();
chr.cancelAllBuffs(true);
chr.closePlayerInteractions();
chr.closePartySearchInteractions();
if (!serverTransition) { // thanks MedicOP for detecting an issue with party leader change on changing channels
removePartyPlayer(c, world);
EventInstanceManager eim = chr.getEventInstance();
if (eim != null) {
eim.playerDisconnected(chr);
}
if (chr.getMonsterCarnival() != null) {
chr.getMonsterCarnival().playerDisconnected(chr.getId());
}
if (chr.getAriantColiseum() != null) {
chr.getAriantColiseum().playerDisconnected(chr);
}
}
if (chr.getMap() != null) {
int mapId = chr.getMapId();
chr.getMap().removePlayer(chr);
if (MapId.isDojo(mapId)) {
c.getChannelServer().freeDojoSectionIfEmpty(mapId);
}
if (chr.getMap().getHPDec() > 0) {
world.removePlayerHpDecrease(chr);
}
}
} catch (final Throwable t) {
log.error("Account stuck", t);
}
}
private void removePartyPlayer(Client c, World world) {
var chr = c.getPlayer();
MapleMap map = chr.getMap();
final Party party = chr.getParty();
final int idz = chr.getId();
if (party != null) {
final PartyCharacter chrp = new PartyCharacter(chr);
chrp.setOnline(false);
world.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) {
world.updateParty(party.getId(), PartyOperation.CHANGE_LEADER, lchr);
}
}
}
}
}