Fixed a bug that was causing players to get stuck logged in

Fixed a bug that would cause players to get stuck in a client crash loop when logging out inside of a POH
Fixed a bug that let players reach objects that shouldn't be reachable
General disconnection reliability improvements
Adjusted the color of global chat for HD mode, the new color is #f1b04c
This commit is contained in:
Ceikry 2023-03-05 11:00:06 +00:00 committed by Ryan
parent c2cb359bf7
commit ff18f69dd0
14 changed files with 91 additions and 151 deletions

View file

@ -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;
}

View file

@ -490,7 +490,7 @@ public class AIPlayer extends Player {
@Override
public void clear() {
botMapping.remove(uid);
super.clear(true);
super.clear();
}
@Override

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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.
*/

View file

@ -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)

View file

@ -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")
}

View file

@ -18,13 +18,14 @@ class DisconnectionQueue {
/**A
* The pending disconnections queue.
*/
private val queue: MutableMap<String, DisconnectionEntry?>
private val queue = HashMap<String, DisconnectionEntry?>()
private val queueTimers = HashMap<String, Int>()
/**
* 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()
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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() {