diff --git a/Server/src/main/content/global/skill/construction/HouseManager.java b/Server/src/main/content/global/skill/construction/HouseManager.java index 49433b087..aa576bef9 100644 --- a/Server/src/main/content/global/skill/construction/HouseManager.java +++ b/Server/src/main/content/global/skill/construction/HouseManager.java @@ -18,6 +18,7 @@ import core.game.world.map.zone.ZoneBorders; import core.game.world.map.zone.ZoneBuilder; import core.game.world.update.flag.context.Animation; import core.tools.Log; +import kotlin.Unit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.simple.JSONArray; @@ -28,7 +29,7 @@ import core.game.world.GameWorld; import java.awt.*; import java.nio.ByteBuffer; -import static core.api.ContentAPIKt.log; +import static core.api.ContentAPIKt.*; import static core.api.regionspec.RegionSpecificationKt.fillWith; import static core.api.regionspec.RegionSpecificationKt.using; @@ -197,6 +198,10 @@ public final class HouseManager { player.lock(1); player.sendMessage("House location: " + houseRegion.getBaseLocation() + ", entry: " + getEnterLocation()); player.getProperties().setTeleportLocation(getEnterLocation()); + registerLogoutListener(player, "houselogout", (p) -> { + p.setLocation(location.getExitLocation()); + return Unit.INSTANCE; + }); openLoadInterface(player); checkForAndSpawnServant(player); updateVarbits(player, buildingMode); @@ -249,6 +254,7 @@ public final class HouseManager { */ public static void leave(Player player) { HouseManager house = player.getAttribute("poh_entry", player.getHouseManager()); + clearLogoutListener(player, "houselogout"); if (house.getHouseRegion() == null){ return; } diff --git a/Server/src/main/core/game/bots/AIPlayer.java b/Server/src/main/core/game/bots/AIPlayer.java index 79b631c14..079875dec 100644 --- a/Server/src/main/core/game/bots/AIPlayer.java +++ b/Server/src/main/core/game/bots/AIPlayer.java @@ -490,7 +490,7 @@ public class AIPlayer extends Player { @Override public void clear() { botMapping.remove(uid); - super.clear(true); + super.clear(); } @Override diff --git a/Server/src/main/core/game/interaction/ScriptProcessor.kt b/Server/src/main/core/game/interaction/ScriptProcessor.kt index ab3e2491b..38069a663 100644 --- a/Server/src/main/core/game/interaction/ScriptProcessor.kt +++ b/Server/src/main/core/game/interaction/ScriptProcessor.kt @@ -259,7 +259,12 @@ class ScriptProcessor(val entity: Entity) { targetDestination = when (interactTarget) { is NPC -> DestinationFlag.ENTITY.getDestination(entity, interactTarget) is Scenery -> { - val path = Pathfinder.find(entity, interactTarget).points.lastOrNull() + val basicPath = Pathfinder.find(entity, interactTarget) + val path = basicPath.points.lastOrNull() + if (basicPath.isMoveNear) { + target.location + return + } if (path == null) { clearScripts(entity) return diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index cf16dde2c..1a3e59533 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -342,26 +342,26 @@ public class Player extends Entity { } super.init(); LoginConfiguration.configureLobby(this); + setAttribute("logged-in-fully", true); } @Override public void clear() { - clear(false); + if (isArtificial()) { + finishClear(); + return; + } + Repository.getDisconnectionQueue().remove(getName()); + Repository.getDisconnectionQueue().add(this, true); + details.save(); } /** * Clears the player from the game. - * @param force If we should force removal, a player engaged in combat will otherwise remain active until out of combat. + * You should NEVER call this manually. This can only be called by the DisconnectionQueue doing its job. + * If you think you need to call this manually, you're wrong. Stop. Turn around. Go back. Here be monsters. */ - public void clear(boolean force) { - if (!isActive()) - return; - if (!force) { - Repository.getDisconnectionQueue().remove(getName()); - Repository.getDisconnectionQueue().add(this, true); - details.save(); - return; - } + public void finishClear() { if (!isArtificial()) GameWorld.getLogoutListeners().forEach((it) -> it.logout(this)); setPlaying(false); @@ -647,7 +647,7 @@ public class Player extends Entity { questRepository = new QuestRepository(this); varpManager = new VarpManager(this); new PlayerSaver(this).save(); - clear(true); + clear(); return; } else { Repository.sendNews("Hardcore Iron " + gender + " " + this.getUsername() + " has fallen. Total Level: " + this.getSkills().getTotalLevel()); // Not enough room for XP diff --git a/Server/src/main/core/game/node/scenery/SceneryBuilder.java b/Server/src/main/core/game/node/scenery/SceneryBuilder.java index 6e2019747..63b7bd8ed 100644 --- a/Server/src/main/core/game/node/scenery/SceneryBuilder.java +++ b/Server/src/main/core/game/node/scenery/SceneryBuilder.java @@ -46,9 +46,7 @@ public final class SceneryBuilder { remove = remove.getWrapper(); Scenery current = LandscapeParser.removeScenery(remove); if (current == null) { - if (GameWorld.getSettings().isDevMode()) { - log(SceneryBuilder.class, Log.ERR, "Object could not be replaced - object to remove is invalid."); - } + log(SceneryBuilder.class, Log.ERR, "Object could not be replaced with " + construct + " - object to remove is invalid."); return false; } if (current.getRestorePulse() != null) { @@ -121,9 +119,7 @@ public final class SceneryBuilder { remove = remove.getWrapper(); Scenery current = LandscapeParser.removeScenery(remove); if (current == null) { - if (GameWorld.getSettings().isDevMode()) { - log(SceneryBuilder.class, Log.ERR, "Object could not be replaced - object to remove is invalid."); - } + log(SceneryBuilder.class, Log.ERR, "Object could not be replaced with " + construct + " - object to remove is invalid."); return false; } if (current.getRestorePulse() != null) { diff --git a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt index d1dc91079..8d83b735a 100644 --- a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt @@ -32,7 +32,6 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) { val farmKitItems = arrayListOf(Items.RAKE_5341, Items.SPADE_952, Items.SEED_DIBBER_5343, Items.WATERING_CAN8_5340, Items.SECATEURS_5329, Items.GARDENING_TROWEL_5325) val runeKitItems = arrayListOf(Items.AIR_RUNE_556, Items.EARTH_RUNE_557, Items.FIRE_RUNE_554, Items.WATER_RUNE_555, Items.MIND_RUNE_558, Items.BODY_RUNE_559, Items.DEATH_RUNE_560, Items.NATURE_RUNE_561, Items.CHAOS_RUNE_562, Items.LAW_RUNE_563, Items.COSMIC_RUNE_564, Items.BLOOD_RUNE_565, Items.SOUL_RUNE_566, Items.ASTRAL_RUNE_9075) override fun defineCommands() { - /** * Gives the player a set of tools used to test farming stuff. */ diff --git a/Server/src/main/core/game/system/command/sets/ModerationCommandSet.kt b/Server/src/main/core/game/system/command/sets/ModerationCommandSet.kt index 459f22220..65d17482b 100644 --- a/Server/src/main/core/game/system/command/sets/ModerationCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/ModerationCommandSet.kt @@ -40,7 +40,7 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){ define("kick", Privilege.MODERATOR){ player, args -> val playerToKick: Player? = Repository.getPlayerByName(args[1]) if (playerToKick != null) { - playerToKick.clear(true) + playerToKick.clear() notify(player, "Player ${playerToKick.username} was kicked.") } else { reject(player, "ERROR REMOVING PLAYER.") @@ -81,7 +81,7 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){ } playerToKick?.details?.accountInfo?.banEndTime = System.currentTimeMillis() + durationMillis - playerToKick?.clear(true) + playerToKick?.clear() GameWorld.Pulser.submit(object : Pulse(2) { override fun pulse(): Boolean { val info = GameWorld.accountStorage.getAccountInfo(name) @@ -131,7 +131,7 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){ for (p in playersToBan) { val playerToKick = Repository.getPlayerByName(p) playerToKick?.details?.accountInfo?.banEndTime = System.currentTimeMillis() + durationMillis - playerToKick?.clear(true) + playerToKick?.clear() GameWorld.Pulser.submit(object : Pulse(2) { override fun pulse(): Boolean { val info = GameWorld.accountStorage.getAccountInfo(p) diff --git a/Server/src/main/core/game/system/communication/GlobalChat.kt b/Server/src/main/core/game/system/communication/GlobalChat.kt index 98e60229a..060849cf7 100644 --- a/Server/src/main/core/game/system/communication/GlobalChat.kt +++ b/Server/src/main/core/game/system/communication/GlobalChat.kt @@ -32,7 +32,7 @@ class GlobalChat : Commands { } private fun prepare(sender: String, message: String, isResizable: Boolean): String { - val baseColor = if (isResizable) "%G" else "%7512ff" + val baseColor = if (isResizable) "%f1b04c" else "%7512ff" val bracketColor = if (isResizable) "%ffffff" else "%000000" return colorize("$bracketColor[${baseColor}G$bracketColor] $sender: ${baseColor}$message") } diff --git a/Server/src/main/core/game/world/repository/DisconnectionQueue.kt b/Server/src/main/core/game/world/repository/DisconnectionQueue.kt index 499f1d9d6..db4e42eaa 100644 --- a/Server/src/main/core/game/world/repository/DisconnectionQueue.kt +++ b/Server/src/main/core/game/world/repository/DisconnectionQueue.kt @@ -18,13 +18,14 @@ class DisconnectionQueue { /**A * The pending disconnections queue. */ - private val queue: MutableMap + private val queue = HashMap() + private val queueTimers = HashMap() /** * Updates all entries. */ fun update() { - if (queue.isEmpty() || GameWorld.ticks % 3 != 0 && GameWorld.settings?.isDevMode != true) { + if (queue.isEmpty() || GameWorld.ticks % 3 != 0) { return } //make a copy of current entries as to avoid concurrency exceptions @@ -33,6 +34,18 @@ class DisconnectionQueue { //loop through entries and disconnect each entries.forEach { if(finish(it.value,false)) queue.remove(it.key) + else { + //Make sure there's no room for the disconnection queue to stroke out and leave someone logged in for 10 years. + queueTimers[it.key] = (queueTimers[it.key] ?: 0) + 3 + if ((queueTimers[it.key] ?: Int.MAX_VALUE) >= 1500) { + it.value?.player?.let { player -> + player.finishClear() + Repository.removePlayer(player) + remove(it.key) + log(this::class.java, Log.WARN, "Force-clearing ${it.key} after 15 minutes of being in the disconnection queue!") + } + } + } } } @@ -51,12 +64,8 @@ class DisconnectionQueue { if (!force && !player.allowRemoval()) { return false } - if (entry.isClear) { - log(this::class.java, Log.FINE, "Clearing player...") - player.clear(true) - } + player.finishClear() Repository.removePlayer(player) - log(this::class.java, Log.INFO, "Player cleared. Removed ${player.details.username}") try { if(player.communication.clan != null) player.communication.clan.leave(player, false) @@ -71,9 +80,11 @@ class DisconnectionQueue { Thread.currentThread().name = "PlayerSave SQL" save(player, true) } + log(this::class.java, Log.INFO, "Player cleared. Removed ${player.details.username}.") return true } save(player, false) + log(this::class.java, Log.INFO, "Player cleared. Removed ${player.details.username}.") return true } @@ -97,94 +108,23 @@ class DisconnectionQueue { queue.clear() } - fun safeClear(){ - for(entry in queue.values){ - finish(entry,false) - } - queue.clear() - } - /** - * Adds a player to the disconnection queue. - * @param player The player. - * @param clear If the player should be cleared. - */ - /** - * Adds a player to the disconnection queue. - * @param player The player. - */ @JvmOverloads fun add(player: Player, clear: Boolean = false) { if(queue[player.name] != null) return queue[player.name] = DisconnectionEntry(player, clear) + log(this::class.java, Log.INFO, "Queueing ${player.name} for disconnection."); } - /** - * Checks if the queue contains the player name. - * @param name The name. - * @return `True` if so. - */ operator fun contains(name: String?): Boolean { return queue.containsKey(name) } - /** - * Removes a queued player. - * @param name The name. - */ fun remove(name: String?) { queue.remove(name) + queueTimers.remove(name) } - /** - * Represents an entry in the disconnection queue, holding the disconnected - * player and time stamp of disconnection. - * @author Emperor - */ - internal inner class DisconnectionEntry( - /** - * The player. - */ - val player: Player, - /** - * If the `Player#clear()` method should be called. - */ - var isClear: Boolean) { - /** - * Gets the timeStamp. - * @return The timeStamp. - */ - /** - * Sets the timeStamp. - * @param timeStamp The timeStamp to set. - */ - /** - * The time of disconnection. - */ - var timeStamp: Int - - /** - * Gets the player. - * @return The player. - */ - - /** - * Gets the clear. - * @return The clear. - */ - /** - * Sets the clear. - * @param isClear The clear to set. - */ - - /** - * Constructs a new `DisconnectionQueue` `Object`. - * @param player The disconnecting player. - * @param clear If the player should be cleared. - */ - init { - timeStamp = GameWorld.ticks - } - } + internal data class DisconnectionEntry(val player: Player, var isClear: Boolean) {} /** * Saves the player. @@ -199,10 +139,4 @@ class DisconnectionQueue { } return false } - /** - * Constructs a new `DisconnectionQueue` `Object`. - */ - init { - queue = ConcurrentHashMap() - } } \ No newline at end of file diff --git a/Server/src/main/core/game/world/repository/Repository.kt b/Server/src/main/core/game/world/repository/Repository.kt index 3468c09c8..08bf2b7c6 100644 --- a/Server/src/main/core/game/world/repository/Repository.kt +++ b/Server/src/main/core/game/world/repository/Repository.kt @@ -142,8 +142,7 @@ object Repository { if (players[i].details.uid == player.details.uid) { val oldPl = players[i] players.remove(oldPl) - oldPl.clear(true) - oldPl.session.disconnect() + oldPl.clear() break; } } diff --git a/Server/src/main/core/net/IoEventHandler.java b/Server/src/main/core/net/IoEventHandler.java index a8b1b7e01..981b70dbf 100644 --- a/Server/src/main/core/net/IoEventHandler.java +++ b/Server/src/main/core/net/IoEventHandler.java @@ -61,8 +61,8 @@ public class IoEventHandler { IoSession session = (IoSession) key.attachment(); try { if (channel.read(buffer) == -1) { - if (session != null && session.getPlayer() != null) { - Repository.getDisconnectionQueue().add(session.getPlayer(), false); + if (session != null) { + session.disconnect(); } key.cancel(); return; @@ -70,8 +70,6 @@ public class IoEventHandler { } catch (IOException e) { if (e.getMessage().contains("reset by peer") && session != null) { session.disconnect(); - if (session.getPlayer() != null) - Repository.getDisconnectionQueue().add(session.getPlayer(), false); } else { key.cancel(); return; diff --git a/Server/src/main/core/net/IoSession.java b/Server/src/main/core/net/IoSession.java index a9a0b0f21..7e61dc28c 100644 --- a/Server/src/main/core/net/IoSession.java +++ b/Server/src/main/core/net/IoSession.java @@ -216,17 +216,12 @@ public class IoSession { key.cancel(); SocketChannel channel = (SocketChannel) key.channel(); channel.socket().close(); - if (object instanceof Player) { - final Player p = getPlayer(); - GameWorld.getPulser().submit(new Pulse(0) { - @Override - public boolean pulse() { - if (p.isActive() && !p.getSession().active) { - p.clear(); - } - return true; - } - }); + if (getPlayer() != null) { + try { + getPlayer().clear(); + } catch (Exception e) { + e.printStackTrace(); + } } object = null; } catch (IOException e) { diff --git a/Server/src/main/core/net/packet/in/Login.kt b/Server/src/main/core/net/packet/in/Login.kt index 15b9d526a..c635f41c4 100644 --- a/Server/src/main/core/net/packet/in/Login.kt +++ b/Server/src/main/core/net/packet/in/Login.kt @@ -26,6 +26,7 @@ import core.game.world.repository.Repository import core.tools.Log import core.worker.ManagementEvents.publish import java.io.File +import java.io.IOException import java.math.BigInteger import java.nio.BufferUnderflowException import java.nio.ByteBuffer @@ -163,8 +164,6 @@ object Login { } catch (e: Exception) { e.printStackTrace() session.disconnect() - Repository.removePlayer(player) - player.clear(true) } } diff --git a/Server/src/main/core/worker/MajorUpdateWorker.kt b/Server/src/main/core/worker/MajorUpdateWorker.kt index dfe79c835..cee3aa942 100644 --- a/Server/src/main/core/worker/MajorUpdateWorker.kt +++ b/Server/src/main/core/worker/MajorUpdateWorker.kt @@ -36,14 +36,18 @@ class MajorUpdateWorker { while (running) { val start = System.currentTimeMillis() Server.heartbeat() - handleTickActions() for (player in Repository.players.filter { !it.isArtificial }) { if (System.currentTimeMillis() - player.session.lastPing > 20000L) { - player?.details?.session?.disconnect() player?.session?.lastPing = Long.MAX_VALUE - player?.clear(true) + player?.session?.disconnect() + } + if (!player.isActive && !Repository.disconnectionQueue.contains(player.name) && player.getAttribute("logged-in-fully", false)) { + //if player has somehow been set as inactive without being queued for disconnection, do that now. This is a failsafe, and should not be relied on. + //if you made a change, and now this is suddenly getting triggered a lot, your change is probably bad. + player?.session?.disconnect() + log(MajorUpdateWorker::class.java, Log.WARN, "Manually disconnecting ${player.name} because they were set as inactive without being disconnected. This is bad.") } } @@ -82,29 +86,34 @@ class MajorUpdateWorker { } fun handleTickActions(skipPulseUpdate: Boolean = false) { - PacketProcessor.processQueue() - - //disconnect all players waiting to be disconnected - Repository.disconnectionQueue.update() - - if (!skipPulseUpdate) { - GameWorld.Pulser.updateAll() - } - GameWorld.tickListeners.forEach { it.tick() } - try { + PacketProcessor.processQueue() + + //disconnect all players waiting to be disconnected + Repository.disconnectionQueue.update() + + if (!skipPulseUpdate) { + GameWorld.Pulser.updateAll() + } + GameWorld.tickListeners.forEach { it.tick() } + sequence.start() sequence.run() sequence.end() + + //increment global ticks variable + GameWorld.pulse() + //tick all manager plugins + Managers.tick() } catch (e: Exception) { e.printStackTrace() + } finally { + try { + PacketWriteQueue.flush() + } catch (e: Exception) { + e.printStackTrace() + } } - //increment global ticks variable - GameWorld.pulse() - //tick all manager plugins - Managers.tick() - - PacketWriteQueue.flush() } fun start() {