Assorted bugfixes

Corrected a confidential issue involving random events
Added Lumbridge tree patch to the farming handler
Rewrote seedling handling in new systems
Fixed the incorrect definition for telegrab's spell ID
Fixed an issue that prevented leaving a clan
Close interfaces when applicable when a walk packet is received
Made the penguin state more reliable
Fixed a somewhat rare concurrency bug with the packet processor
Refactored DialogueOption packet to ContinueOption, due to a revelation that it was not particular to dialogue
Fixed a new, unreported issue with the stats interface
This commit is contained in:
Ceikry 2022-12-23 06:35:06 +00:00 committed by Ryan
parent 5011b8648b
commit 9b6a817c9d
11 changed files with 164 additions and 105 deletions

View file

@ -79,7 +79,7 @@ public final class TelekineticGrabSpell extends MagicSpell {
/** /**
* Represents the spell id. * Represents the spell id.
*/ */
public static final int SPELL_ID = 65535; public static final int SPELL_ID = 19;
/** /**
* Constructs a new {@code TelekineticGrabSpell} {@code Object}. * Constructs a new {@code TelekineticGrabSpell} {@code Object}.

View file

@ -1,6 +1,8 @@
package api package api
import core.game.node.item.Item import core.game.node.item.Item
import java.util.*
import kotlin.collections.ArrayList
fun IntRange.toIntArray(): IntArray { fun IntRange.toIntArray(): IntArray {
if (last < first) if (last < first)
@ -54,3 +56,8 @@ fun IntArray.getNext(element: Int) : Int {
fun IntArray.isNextLast(element: Int) : Boolean { fun IntArray.isNextLast(element: Int) : Boolean {
return this.isLast(this.getNext(element)) return this.isLast(this.getNext(element))
} }
fun <T> LinkedList<T>.tryPop(default: T) : T {
this.peek() ?: return default
return this.pop()
}

View file

@ -1,5 +1,6 @@
package rs09.game.content.ame package rs09.game.content.ame
import api.Commands
import api.LoginListener import api.LoginListener
import api.events.EventHook import api.events.EventHook
import api.events.TickEvent import api.events.TickEvent
@ -11,10 +12,12 @@ import core.game.world.map.zone.ZoneRestriction
import core.tools.RandomFunction import core.tools.RandomFunction
import rs09.game.Event import rs09.game.Event
import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger
import rs09.game.system.command.Privilege
import rs09.game.world.GameWorld import rs09.game.world.GameWorld
import rs09.game.world.repository.Repository
import kotlin.random.Random import kotlin.random.Random
class RandomEventManager(val player: Player? = null) : LoginListener, EventHook<TickEvent> { class RandomEventManager(val player: Player? = null) : LoginListener, EventHook<TickEvent>, Commands {
var event: RandomEventNPC? = null var event: RandomEventNPC? = null
var enabled: Boolean = false var enabled: Boolean = false
var nextSpawn = 0 var nextSpawn = 0
@ -71,6 +74,18 @@ class RandomEventManager(val player: Player? = null) : LoginListener, EventHook<
nextSpawn = GameWorld.ticks + RandomFunction.random(MIN_DELAY_TICKS, MAX_DELAY_TICKS) nextSpawn = GameWorld.ticks + RandomFunction.random(MIN_DELAY_TICKS, MAX_DELAY_TICKS)
} }
override fun defineCommands() {
define("targeted-ame", Privilege.ADMIN, "targeted-ame username", "Summons a random for the given user") {player, args ->
val username = args[1]
val target = Repository.getPlayerByName(username)
if (target == null)
reject(player, "Unable to find player $username!")
getInstance(target!!)?.fireEvent()
}
}
companion object { companion object {
const val AVG_DELAY_TICKS = 6000 // 60 minutes const val AVG_DELAY_TICKS = 6000 // 60 minutes
const val MIN_DELAY_TICKS = AVG_DELAY_TICKS / 2 const val MIN_DELAY_TICKS = AVG_DELAY_TICKS / 2

View file

@ -43,12 +43,14 @@ class PenguinManager{
} }
fun rebuildVars() { fun rebuildVars() {
if(PenguinHNSEvent.getStoreFile().isEmpty()) { if(!PenguinHNSEvent.getStoreFile().containsKey("spawned-penguins")) {
penguins = spawner.spawnPenguins(10) penguins = spawner.spawnPenguins(10)
PenguinHNSEvent.getStoreFile()["spawned-penguins"] = penguins.toJSONArray() PenguinHNSEvent.getStoreFile()["spawned-penguins"] = penguins.toJSONArray()
tagMapping.clear()
for (p in penguins) { for (p in penguins) {
tagMapping.put(p, JSONArray()) tagMapping.put(p, JSONArray())
} }
updateStoreFile()
} else { } else {
val spawnedOrdinals = (PenguinHNSEvent.getStoreFile()["spawned-penguins"] as JSONArray).map { it.toString().toInt() } val spawnedOrdinals = (PenguinHNSEvent.getStoreFile()["spawned-penguins"] as JSONArray).map { it.toString().toInt() }
spawner.spawnPenguins(spawnedOrdinals) spawner.spawnPenguins(spawnedOrdinals)

View file

@ -33,7 +33,7 @@ class NPCTalkListener : InteractionListener {
on(IntType.NPC,"talk-to","talk","talk to"){player,node -> on(IntType.NPC,"talk-to","talk","talk to"){player,node ->
val npc = node.asNpc() val npc = node.asNpc()
if(RandomEvents.randomIDs.contains(node.id)){ if(RandomEvents.randomIDs.contains(node.id)){
if(RandomEventManager.getInstance(player)!!.event == null || RandomEventManager.getInstance(player)!!.event!!.id != node.id){ if(RandomEventManager.getInstance(player)!!.event == null || RandomEventManager.getInstance(player)!!.event!! != node.asNpc()){
player.sendMessage("They aren't interested in talking to you.") player.sendMessage("They aren't interested in talking to you.")
} else { } else {
RandomEventManager.getInstance(player)!!.event!!.talkTo(node.asNpc()) RandomEventManager.getInstance(player)!!.event!!.talkTo(node.asNpc())

View file

@ -1,31 +1,70 @@
package rs09.game.node.entity.skill.farming package rs09.game.node.entity.skill.farming
import core.game.interaction.NodeUsageEvent import api.addItem
import core.game.interaction.UseWithHandler import api.inInventory
import core.game.node.item.Item import api.removeItem
import core.plugin.Initializable import api.sendDialogue
import core.plugin.Plugin import core.game.node.Node
import core.game.node.entity.player.Player
import org.rs09.consts.Items import org.rs09.consts.Items
import rs09.game.interaction.IntType
import rs09.game.interaction.InteractionListener
import rs09.game.node.entity.state.newsys.states.SeedlingState
@Initializable class SeedlingListener : InteractionListener {
class SeedOnPlantPot : UseWithHandler(Items.ACORN_5312, override fun defineListeners() {
Items.WILLOW_SEED_5313,Items.MAPLE_SEED_5314,Items.YEW_SEED_5315,Items.MAGIC_SEED_5316,Items.APPLE_TREE_SEED_5283,Items.BANANA_TREE_SEED_5284,Items.ORANGE_TREE_SEED_5285,Items.CURRY_TREE_SEED_5286,Items.PINEAPPLE_SEED_5287,Items.PAPAYA_TREE_SEED_5288,Items.PALM_TREE_SEED_5289) { onUseWith(IntType.ITEM, TREE_SEEDS, Items.PLANT_POT_5354, handler = ::addSeedToPot)
override fun newInstance(arg: Any?): Plugin<Any> { onUseWith(IntType.ITEM, TREE_SEEDLINGS, *WATERING_CANS, handler = ::waterSeedling)
addHandler(Items.PLANT_POT_5354, ITEM_TYPE,this)
return this
} }
override fun handle(event: NodeUsageEvent?): Boolean { fun addSeedToPot(player: Player, used: Node, with: Node) : Boolean {
val player = event?.player ?: return false val seed = used.asItem() ?: return false
val pot = event.usedItem ?: return false val pot = with.asItem() ?: return false
val seed = event.usedWith.asItem() ?: return false
if(!player.inventory.contains(Items.GARDENING_TROWEL_5325,1)){ if(!inInventory(player, Items.GARDENING_TROWEL_5325)){
player.dialogueInterpreter.sendDialogue("You need a gardening trowel on you to do this.") sendDialogue(player, "You need a gardening trowel on you to do this.")
return false return false
} }
val seedling = when(seed.id){ val seedling = getSeedling(seed.id)
if (seedling == -1) return false
if (!removeItem(player, seed) || !removeItem(player, pot)) return true
addItem(player, seedling)
return true
}
fun waterSeedling(player: Player, used: Node, with: Node) : Boolean {
val seedling = used.asItem() ?: return false
val can = with.asItem() ?: return false
val nextCan = can.id.getNext()
val wateredSeedling = if (seedling.id > 5400) seedling.id + 8 else seedling.id + 6
if (!removeItem(player, can) || !removeItem(player, seedling)) return false
addItem(player, wateredSeedling)
addItem(player, nextCan)
var state = player.states["seedling"] as SeedlingState?
if (state != null) {
state.addSeedling(wateredSeedling)
return true
}
state = player.registerState("seedling") as SeedlingState?
state?.addSeedling(wateredSeedling)
state?.init()
return true
}
private fun Int.getNext(): Int{
val index = WATERING_CANS.indexOf(this)
if (index == -1) return Items.WATERING_CAN_5331
return if (index != WATERING_CANS.size -1) WATERING_CANS[index + 1] else Items.WATERING_CAN_5331
}
fun getSeedling(id: Int) : Int {
return when (id) {
Items.ACORN_5312 -> Items.OAK_SEEDLING_5358 Items.ACORN_5312 -> Items.OAK_SEEDLING_5358
Items.WILLOW_SEED_5313 -> Items.WILLOW_SEEDLING_5359 Items.WILLOW_SEED_5313 -> Items.WILLOW_SEEDLING_5359
Items.MAPLE_SEED_5314 -> Items.MAPLE_SEEDLING_5360 Items.MAPLE_SEED_5314 -> Items.MAPLE_SEEDLING_5360
@ -38,13 +77,42 @@ class SeedOnPlantPot : UseWithHandler(Items.ACORN_5312,
Items.PINEAPPLE_SEED_5287 -> Items.PINEAPPLE_SEEDLING_5484 Items.PINEAPPLE_SEED_5287 -> Items.PINEAPPLE_SEEDLING_5484
Items.PAPAYA_TREE_SEED_5288 -> Items.PAPAYA_SEEDLING_5485 Items.PAPAYA_TREE_SEED_5288 -> Items.PAPAYA_SEEDLING_5485
Items.PALM_TREE_SEED_5289 -> Items.PALM_SEEDLING_5486 Items.PALM_TREE_SEED_5289 -> Items.PALM_SEEDLING_5486
else -> return false Items.SPIRIT_SEED_5317 -> Items.SPIRIT_SEEDLING_5363
else -> -1
}
} }
if(player.inventory.remove(pot) && player.inventory.remove(Item(seed.id,1))){ val TREE_SEEDS = intArrayOf(
player.inventory.add(Item(seedling)) Items.ACORN_5312,
} Items.WILLOW_SEED_5313,
Items.MAPLE_SEED_5314,
Items.YEW_SEED_5315,
Items.MAGIC_SEED_5316,
Items.APPLE_TREE_SEED_5283,
Items.BANANA_TREE_SEED_5284,
Items.ORANGE_TREE_SEED_5285,
Items.CURRY_TREE_SEED_5286,
Items.PINEAPPLE_SEED_5287,
Items.PAPAYA_TREE_SEED_5288,
Items.PALM_TREE_SEED_5289,
Items.SPIRIT_SEED_5317
)
return true val TREE_SEEDLINGS = intArrayOf(
} Items.OAK_SEEDLING_5358,
Items.WILLOW_SEEDLING_5359,
Items.MAPLE_SEEDLING_5360,
Items.YEW_SEEDLING_5361,
Items.MAGIC_SEEDLING_5362,
Items.APPLE_SEEDLING_5480,
Items.BANANA_SEEDLING_5481,
Items.ORANGE_SEEDLING_5482,
Items.CURRY_SEEDLING_5483,
Items.PINEAPPLE_SEEDLING_5484,
Items.PAPAYA_SEEDLING_5485,
Items.PALM_SEEDLING_5486,
Items.SPIRIT_SEEDLING_5363
)
private val WATERING_CANS = intArrayOf(Items.WATERING_CAN8_5340,Items.WATERING_CAN7_5339,Items.WATERING_CAN6_5338,Items.WATERING_CAN5_5337,Items.WATERING_CAN4_5336,Items.WATERING_CAN3_5335,Items.WATERING_CAN2_5334,Items.WATERING_CAN1_5333)
} }

View file

@ -1,54 +0,0 @@
package rs09.game.node.entity.skill.farming
import core.game.interaction.NodeUsageEvent
import core.game.interaction.UseWithHandler
import core.game.node.item.Item
import core.plugin.Initializable
import core.plugin.Plugin
import org.rs09.consts.Items
import rs09.game.node.entity.state.newsys.states.SeedlingState
private val cans = arrayListOf(Items.WATERING_CAN8_5340,Items.WATERING_CAN7_5339,Items.WATERING_CAN6_5338,Items.WATERING_CAN5_5337,Items.WATERING_CAN4_5336,Items.WATERING_CAN3_5335,Items.WATERING_CAN2_5334,Items.WATERING_CAN1_5333)
private val seedlings = arrayListOf(Items.OAK_SEEDLING_5358,Items.WILLOW_SEEDLING_5359,Items.MAPLE_SEEDLING_5360,Items.YEW_SEEDLING_5361,Items.MAGIC_SEEDLING_5362,Items.APPLE_SEEDLING_5480,Items.BANANA_SEEDLING_5481,Items.ORANGE_SEEDLING_5482,Items.CURRY_SEEDLING_5483,Items.PINEAPPLE_SEEDLING_5484,Items.PAPAYA_SEEDLING_5485,Items.PALM_SEEDLING_5486)
@Initializable
class SeedlingWaterer : UseWithHandler(*cans.toIntArray()) {
override fun newInstance(arg: Any?): Plugin<Any> {
for(seed in seedlings) addHandler(seed, ITEM_TYPE,this)
return this
}
override fun handle(event: NodeUsageEvent?): Boolean {
val player = event?.player ?: return false
val seedling = event.used.asItem() ?: return false
val can = event.usedWith.asItem() ?: return false
val nextCan = can.id.getNext()
val wateredSeedling = if(seedling.id > 5400 ) seedling.id + 8 else seedling.id + 6
if(player.inventory.remove(can) && player.inventory.remove(seedling)){
player.inventory.add(Item(wateredSeedling))
player.inventory.add(Item(nextCan))
var state = player.states["seedling"] as SeedlingState?
if(state == null){
state = player.registerState("seedling") as SeedlingState?
state?.addSeedling(wateredSeedling)
state?.init()
} else {
state.addSeedling(wateredSeedling)
}
player.sendMessage("You water the seedling.")
}
return true
}
private fun Int.getNext(): Int{
var index = cans.indexOf(this)
if(index == -1) return Items.WATERING_CAN_5331
if(index != cans.size -1) return cans[index + 1] else return Items.WATERING_CAN_5331
}
}

View file

@ -232,7 +232,7 @@ class UseWithPatchHandler : InteractionListener {
patches.addAll(8550..8557) //allotment wrappers patches.addAll(8550..8557) //allotment wrappers
patches.addAll(7847..7853) //flower patch wrappers patches.addAll(7847..7853) //flower patch wrappers
patches.addAll(8150..8156) //herb patch wrappers patches.addAll(8150..8156) //herb patch wrappers
patches.addAll(8388..8390) // Tree patches patches.addAll(8388..8391) // Tree patches
patches.add(19147) //Tree patch patches.add(19147) //Tree patch
patches.addAll(7962..7965) //fruit trees patches.addAll(7962..7965) //fruit trees
patches.addAll(8173..8176) //hops patches.addAll(8173..8176) //hops

View file

@ -3,6 +3,7 @@ package rs09.net.packet
import api.events.ButtonClickEvent import api.events.ButtonClickEvent
import api.getAttribute import api.getAttribute
import api.sendMessage import api.sendMessage
import api.tryPop
import core.cache.def.impl.ItemDefinition import core.cache.def.impl.ItemDefinition
import core.cache.def.impl.NPCDefinition import core.cache.def.impl.NPCDefinition
import core.cache.def.impl.SceneryDefinition import core.cache.def.impl.SceneryDefinition
@ -17,7 +18,6 @@ import core.game.interaction.NodeUsageEvent
import core.game.interaction.Option import core.game.interaction.Option
import core.game.interaction.UseWithHandler import core.game.interaction.UseWithHandler
import core.game.node.Node import core.game.node.Node
import core.game.node.entity.impl.PulseType
import core.game.node.entity.player.Player import core.game.node.entity.player.Player
import core.game.node.entity.player.info.Rights import core.game.node.entity.player.info.Rights
import core.game.node.entity.player.info.login.LoginConfiguration import core.game.node.entity.player.info.login.LoginConfiguration
@ -43,8 +43,8 @@ import discord.Discord
import org.rs09.consts.Components import org.rs09.consts.Components
import proto.management.ClanMessage import proto.management.ClanMessage
import proto.management.JoinClanRequest import proto.management.JoinClanRequest
import proto.management.LeaveClanRequest
import rs09.ServerConstants import rs09.ServerConstants
import rs09.game.ge.GrandExchange
import rs09.game.ge.GrandExchange.Companion.getOfferStats import rs09.game.ge.GrandExchange.Companion.getOfferStats
import rs09.game.ge.GrandExchange.Companion.getRecommendedPrice import rs09.game.ge.GrandExchange.Companion.getRecommendedPrice
import rs09.game.ge.GrandExchangeOffer import rs09.game.ge.GrandExchangeOffer
@ -81,7 +81,7 @@ object PacketProcessor {
val pw = PrintWriter(sw) val pw = PrintWriter(sw)
var pkt: Packet var pkt: Packet
while (countThisCycle-- > 0) { while (countThisCycle-- > 0) {
pkt = queue.pop() pkt = queue.tryPop(Packet.NoProcess())
try { try {
process(pkt) process(pkt)
} catch (e: Exception) { } catch (e: Exception) {
@ -127,7 +127,13 @@ object PacketProcessor {
is Packet.PlayerPrefsUpdate -> {/*TODO implement something that cares about this */} is Packet.PlayerPrefsUpdate -> {/*TODO implement something that cares about this */}
is Packet.Ping -> pkt.player.session.lastPing = System.currentTimeMillis() is Packet.Ping -> pkt.player.session.lastPing = System.currentTimeMillis()
is Packet.JoinClan -> { is Packet.JoinClan -> {
if (pkt.clanName.isNotEmpty()) if (pkt.clanName.isEmpty() && pkt.player.communication.currentClan.isNotEmpty()) {
val builder = LeaveClanRequest.newBuilder()
builder.clanName = pkt.player.communication.currentClan
builder.username = pkt.player.name
ManagementEvents.publish(builder.build())
return
}
sendMessage(pkt.player, "Attempting to join channel....:clan:") sendMessage(pkt.player, "Attempting to join channel....:clan:")
val builder = JoinClanRequest.newBuilder() val builder = JoinClanRequest.newBuilder()
builder.clanName = pkt.clanName builder.clanName = pkt.clanName
@ -228,11 +234,19 @@ object PacketProcessor {
val final = pkt.count - pkt.player.interfaceManager.getPacketCount(0) val final = pkt.count - pkt.player.interfaceManager.getPacketCount(0)
pkt.player.interfaceManager.getPacketCount(final) pkt.player.interfaceManager.getPacketCount(final)
} }
is Packet.DialogueOption -> { is Packet.ContinueOption -> {
val player = pkt.player val player = pkt.player
player.debug("[CONTINUE OPT]----------")
player.debug("Iface: ${pkt.iface}")
player.debug("Child: ${pkt.child}")
player.debug("Slot: ${pkt.slot}")
player.debug("------------------------")
if (player.dialogueInterpreter.dialogue == null) { if (player.dialogueInterpreter.dialogue == null) {
player.interfaceManager.closeChatbox() player.interfaceManager.closeChatbox()
player.dialogueInterpreter.actions.removeFirstOrNull()?.handle(player, pkt.child) player.dialogueInterpreter.actions.removeFirstOrNull()?.handle(player, pkt.child)
val component = player.interfaceManager.getComponent(pkt.iface) ?: return
if (!InterfaceListeners.run(player, component, pkt.opcode, pkt.child, pkt.slot, -1))
component.plugin?.handle(player, component, pkt.opcode, pkt.child, pkt.slot, -1)
return return
} }
player.dialogueInterpreter.handle(pkt.iface, pkt.child) player.dialogueInterpreter.handle(pkt.iface, pkt.child)
@ -427,7 +441,14 @@ object PacketProcessor {
//there's more data in this packet, we're just not using it //there's more data in this packet, we're just not using it
} }
if (player.locks.isMovementLocked || !player.interfaceManager.close() || !player.interfaceManager.closeSingleTab() || !player.dialogueInterpreter.close()) { var canWalk = !player.locks.isMovementLocked
if (canWalk && player.interfaceManager.isOpened && !player.interfaceManager.opened.definition.isWalkable)
canWalk = canWalk && player.interfaceManager.close()
if (canWalk && player.interfaceManager.hasChatbox() && !player.interfaceManager.chatbox.definition.isWalkable)
player.interfaceManager.closeChatbox()
if (!canWalk || !player.dialogueInterpreter.close()) {
player.debug("[WALK ACTION]-- NO HANDLE: PLAYER LOCKED OR INTERFACES SAY NO") player.debug("[WALK ACTION]-- NO HANDLE: PLAYER LOCKED OR INTERFACES SAY NO")
return sendClearMinimap(player) return sendClearMinimap(player)
} }

View file

@ -349,12 +349,12 @@ enum class Decoders530(val opcode: Int) {
return Packet.IfAction(player, opcode, -1, iface, button, -1, -1) return Packet.IfAction(player, opcode, -1, iface, button, -1, -1)
} }
}, },
DIALOGUE_OPT(132) { CONTINUE_OPT(132) {
override fun decode(player: Player, buffer: IoBuffer): Packet { override fun decode(player: Player, buffer: IoBuffer): Packet {
val ifHash = buffer.intA val ifHash = buffer.intA
val slot = buffer.leShort val slot = buffer.leShort
val (iface, button) = deHash(ifHash) val (iface, button) = deHash(ifHash)
return Packet.DialogueOption(player, iface, button) return Packet.ContinueOption(player, iface, button, slot, 132)
} }
}, },
CLOSE_IFACE(184) { CLOSE_IFACE(184) {
@ -364,8 +364,8 @@ enum class Decoders530(val opcode: Int) {
}, },
IF_GROUNDITEM_ACTION(73) { IF_GROUNDITEM_ACTION(73) {
override fun decode(player: Player, buffer: IoBuffer): Packet { override fun decode(player: Player, buffer: IoBuffer): Packet {
val child = buffer.short val ifHash = buffer.intA
val iface = buffer.short val (iface, child) = deHash(ifHash)
val y = buffer.short val y = buffer.short
val itemId = buffer.leShortA val itemId = buffer.leShortA
val x = buffer.leShortA val x = buffer.leShortA

View file

@ -17,7 +17,7 @@ sealed class Packet {
data class UseWithScenery(val player: Player, val itemId: Int, val slot: Int, val sceneryId: Int, val x: Int, val y: Int) : Packet() data class UseWithScenery(val player: Player, val itemId: Int, val slot: Int, val sceneryId: Int, val x: Int, val y: Int) : Packet()
data class UseWithGroundItem(val player: Player, val usedId: Int, val withId: Int, val iface: Int, val child: Int, val slot: Int, val x: Int, val y: Int) : Packet() data class UseWithGroundItem(val player: Player, val usedId: Int, val withId: Int, val iface: Int, val child: Int, val slot: Int, val x: Int, val y: Int) : Packet()
data class IfAction(val player: Player, val opcode: Int, val optIndex: Int, val iface: Int, val child: Int, val slot: Int, val itemId: Int = -1) : Packet() data class IfAction(val player: Player, val opcode: Int, val optIndex: Int, val iface: Int, val child: Int, val slot: Int, val itemId: Int = -1) : Packet()
data class DialogueOption(val player: Player, val iface: Int, val child: Int) : Packet() data class ContinueOption(val player: Player, val iface: Int, val child: Int, val slot: Int, val opcode: Int) : Packet()
data class CloseIface(val player: Player) : Packet() data class CloseIface(val player: Player) : Packet()
data class JoinClan(val player: Player, val clanName: String) : Packet() data class JoinClan(val player: Player, val clanName: String) : Packet()
data class SetClanRank(val player: Player, val username: String, val rank: Int) : Packet() data class SetClanRank(val player: Player, val username: String, val rank: Int) : Packet()