fix outfit rank names not representing DB values

add MOTD handling
renaming OMR packet types with known uses
handling outfit promotions (setrank)
handle outfit owner changes
changing the migration to change to unique index. allows concurrent refresh of MV
This commit is contained in:
Resaec 2025-08-30 01:35:51 +02:00
parent ad52c8076c
commit 18dd426d13
6 changed files with 227 additions and 37 deletions

View file

@ -104,4 +104,4 @@ CREATE MATERIALIZED VIEW outfitpoint_mv AS
"outfitpoint" "outfitpoint"
GROUP BY "outfit_id"; GROUP BY "outfit_id";
CREATE INDEX "outfitpoint_mv_outfit_id_idx" ON "outfitpoint_mv" ("outfit_id"); CREATE UNIQUE INDEX "outfitpoint_mv_outfit_id_unique" ON "outfitpoint_mv" ("outfit_id");

View file

@ -830,14 +830,13 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleOutfitRequest(pkt: OutfitRequest): Unit = { def handleOutfitRequest(pkt: OutfitRequest): Unit = {
pkt match { pkt match {
case OutfitRequest(_, OutfitRequestAction.Motd(message)) =>
SessionOutfitHandlers.HandleOutfitMotd(zones, message, player)
case OutfitRequest(_, OutfitRequestAction.Ranks(List(r1, r2, r3, r4, r5, r6, r7, r8))) => case OutfitRequest(_, OutfitRequestAction.Ranks(List(r1, r2, r3, r4, r5, r6, r7, r8))) =>
// update db // update db
//sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames(r1.getOrElse(""), r2.getOrElse(""), r3.getOrElse(""), r4.getOrElse(""), r5.getOrElse(""), r6.getOrElse(""), r7.getOrElse(""), r8.getOrElse("")), "Welcome to the first PSForever Outfit!", 0, unk11=true, 0, 8888888, 0, 0, 0)))) //sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames(r1.getOrElse(""), r2.getOrElse(""), r3.getOrElse(""), r4.getOrElse(""), r5.getOrElse(""), r6.getOrElse(""), r7.getOrElse(""), r8.getOrElse("")), "Welcome to the first PSForever Outfit!", 0, unk11=true, 0, 8888888, 0, 0, 0))))
case OutfitRequest(_, OutfitRequestAction.Motd(message)) =>
// update db
//sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames("", "", "", "", "", "", "", ""), message, 0, unk11=true, 0, 8888888, 0, 0, 0))))
case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => case OutfitRequest(_, OutfitRequestAction.Unk3(true)) =>
SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id) SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id)

View file

@ -1,7 +1,7 @@
// Copyright (c) 2025 PSForever // Copyright (c) 2025 PSForever
package net.psforever.actors.session.support package net.psforever.actors.session.support
import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase} import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase, Update}
import net.psforever.objects.avatar.PlayerControl import net.psforever.objects.avatar.PlayerControl
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.Player import net.psforever.objects.Player
@ -19,7 +19,14 @@ import scala.util.{Failure, Success}
object SessionOutfitHandlers { object SessionOutfitHandlers {
case class Avatar(id: Long, name: String, faction_id: Int, last_login: java.time.LocalDateTime) case class Avatar(id: Long, name: String, faction_id: Int, last_login: java.time.LocalDateTime)
case class Outfit(id: Long, name: String, faction: Int, owner_id: Long, motd: Option[String], created: java.time.LocalDateTime, case class Outfit(
id: Long,
name: String,
faction: Int,
owner_id: Long,
motd: Option[String],
created: java.time.LocalDateTime,
deleted: Boolean,
rank0: Option[String], rank0: Option[String],
rank1: Option[String], rank1: Option[String],
rank2: Option[String], rank2: Option[String],
@ -27,7 +34,8 @@ object SessionOutfitHandlers {
rank4: Option[String], rank4: Option[String],
rank5: Option[String], rank5: Option[String],
rank6: Option[String], rank6: Option[String],
rank7: Option[String]) rank7: Option[String]
)
case class Outfitmember(id: Long, outfit_id: Long, avatar_id: Long, rank: Int) case class Outfitmember(id: Long, outfit_id: Long, avatar_id: Long, rank: Int)
case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Option[Long], points: Long) case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Option[Long], points: Long)
case class OutfitpointMv(outfit_id: Long, points: Long) case class OutfitpointMv(outfit_id: Long, points: Long)
@ -126,12 +134,12 @@ object SessionOutfitHandlers {
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
OutfitMembershipResponse( OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.Unk2, 0, 0, OutfitMembershipResponse.PacketType.InviteAccepted, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = false)) invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = false))
PlayerControl.sendResponse(invited.Zone, invited.Name, PlayerControl.sendResponse(invited.Zone, invited.Name,
OutfitMembershipResponse( OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.Unk2, 0, 0, OutfitMembershipResponse.PacketType.InviteAccepted, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = true)) invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = true))
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
@ -183,12 +191,12 @@ object SessionOutfitHandlers {
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
OutfitMembershipResponse( OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.Unk3, 0, 0, OutfitMembershipResponse.PacketType.InviteRejected, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = false)) invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = false))
PlayerControl.sendResponse(invited.Zone, invited.Name, PlayerControl.sendResponse(invited.Zone, invited.Name,
OutfitMembershipResponse( OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.Unk3, 0, 0, OutfitMembershipResponse.PacketType.InviteRejected, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = true)) invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = true))
OutfitInviteManager.removeOutfitInvite(invited.CharId) OutfitInviteManager.removeOutfitInvite(invited.CharId)
@ -277,10 +285,60 @@ object SessionOutfitHandlers {
} }
def HandleOutfitPromote(zones: Seq[Zone], promotedId: Long, newRank: Int, promoter: Player): Unit = { def HandleOutfitPromote(zones: Seq[Zone], promotedId: Long, newRank: Int, promoter: Player): Unit = {
// send to all online players in outfit
val outfit_id = promoter.outfit_id
findPlayerByIdForOutfitAction(zones, promotedId, promoter).foreach { promoted => findPlayerByIdForOutfitAction(zones, promotedId, promoter).foreach { promoted =>
PlayerControl.sendResponse(promoted.Zone, promoted.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0)))
PlayerControl.sendResponse(promoter.Zone, promoter.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0))) if (newRank == 7) {
// demote owner to rank 6
// promote promoted to rank 7
// update outfit
updateOutfitOwner(outfit_id, promoter.avatar.id, promoted.avatar.id)
// TODO: does every member get the notification like this?
getOutfitMemberPoints(outfit_id, promoter.avatar.id).map {
owner_points =>
// announce owner rank change
zones.foreach(zone => {
zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(outfitMember => {
PlayerControl.sendResponse(
zone, outfitMember.Name,
OutfitMemberEvent(outfit_id, promoter.avatar.id,
OutfitMemberEventAction.Unk0(promoter.Name, 6, owner_points, 0, OutfitMemberEventAction.PacketType.Padding, 0)))
})
})
}
// update promoter rank
PlayerControl.sendResponse(
promoter.Zone, promoter.Name,
OutfitMemberUpdate(outfit_id, promoter.avatar.id, rank = 6, flag = true))
}
else {
// promote promoted
updateOutfitMemberRank(outfit_id, promoted.avatar.id, rank = newRank)
}
// TODO: does every member get the notification like this?
getOutfitMemberPoints(outfit_id, promoted.avatar.id).map {
member_points =>
// tell everyone about the new rank of the promoted member
zones.foreach(zone => {
zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(player => {
PlayerControl.sendResponse(
zone, player.Name,
OutfitMemberEvent(outfit_id, promoted.avatar.id,
OutfitMemberEventAction.Unk0(promoted.Name, newRank, member_points, 0, OutfitMemberEventAction.PacketType.Padding, 0)))
})
})
}
// update promoted rank
PlayerControl.sendResponse(
promoted.Zone, promoted.Name,
OutfitMemberUpdate(outfit_id, promoted.avatar.id, rank = newRank, flag = true))
} }
} }
@ -306,7 +364,16 @@ object SessionOutfitHandlers {
totalPoints, totalPoints,
totalPoints, totalPoints,
memberCount, memberCount,
OutfitRankNames("", "", "", "", "", "", "", ""), OutfitRankNames(
outfit.rank0.getOrElse(""),
outfit.rank1.getOrElse(""),
outfit.rank2.getOrElse(""),
outfit.rank3.getOrElse(""),
outfit.rank4.getOrElse(""),
outfit.rank5.getOrElse(""),
outfit.rank6.getOrElse(""),
outfit.rank7.getOrElse(""),
),
outfit.motd.getOrElse(""), outfit.motd.getOrElse(""),
14, unk11 = true, 0, seconds, 0, 0, 0)))) 14, unk11 = true, 0, seconds, 0, 0, 0))))
@ -355,6 +422,71 @@ object SessionOutfitHandlers {
} }
} }
def HandleOutfitMotd(zones: Seq[Zone], message: String, player: Player): Unit = {
val outfit_id = player.outfit_id
// update MOTD
updateOutfitMotd(outfit_id, message)
// TODO this does not notify clients with open windows. Do they update in the first place?
val outfitDetails = for {
outfitOpt <- ctx.run(getOutfitById(outfit_id)).map(_.headOption)
memberCount <- ctx.run(query[Outfitmember].filter(_.outfit_id == lift(outfit_id)).size)
pointsTotal <- ctx.run(querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(outfit_id)))
} yield (outfitOpt, memberCount, pointsTotal.headOption.map(_.points).getOrElse(0L))
for {
(outfitOpt, memberCount, totalPoints) <- outfitDetails
} yield {
outfitOpt.foreach { outfit =>
// send to all online players in outfit
val outfit_event = OutfitEvent(
outfit_id,
Unk2(
OutfitInfo(
outfit_name = outfit.name,
outfit_points1 = totalPoints,
outfit_points2 = totalPoints,
member_count = memberCount,
outfit_rank_names = OutfitRankNames(
outfit.rank0.getOrElse(""),
outfit.rank1.getOrElse(""),
outfit.rank2.getOrElse(""),
outfit.rank3.getOrElse(""),
outfit.rank4.getOrElse(""),
outfit.rank5.getOrElse(""),
outfit.rank6.getOrElse(""),
outfit.rank7.getOrElse(""),
),
motd = outfit.motd.getOrElse(""),
unk10 = 0,
unk11 = true,
unk12 = 0,
created_timestamp = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000,
unk23 = 0,
unk24 = 0,
unk25 = 0
)
)
)
zones.foreach(zone => {
zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(player => {
PlayerControl.sendResponse(
zone, player.Name,
outfit_event
)
})
})
}
}
// C >> S OutfitRequest(41593365, Motd(Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net))
// S >> C OutfitEvent(Unk2, 529744, Unk2(OutfitInfo(PlanetSide_Forever_Vanu, 0, 0, 3, OutfitRankNames(, , , , , , , ), Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net, 0, 1, 0, 1458331641, 0, 0, 0)))
}
/* supporting functions */ /* supporting functions */
def sanitizeOutfitName(name: String): Option[String] = { def sanitizeOutfitName(name: String): Option[String] = {
@ -435,12 +567,14 @@ object SessionOutfitHandlers {
for { for {
deleted <- ctx.run( deleted <- ctx.run(
query[Outfitmember] query[Outfitmember]
.filter(m => m.outfit_id == lift(outfit_id) && m.avatar_id == lift(avatar_id)) .filter(_.outfit_id == lift(outfit_id))
.filter(_.avatar_id == lift(avatar_id))
.delete .delete
) )
updated <- ctx.run( updated <- ctx.run(
query[Outfitpoint] query[Outfitpoint]
.filter(p => p.outfit_id == lift(outfit_id) && p.avatar_id == lift(avatarOpt)) .filter(_.outfit_id == lift(outfit_id))
.filter(_.avatar_id == lift(avatarOpt))
.update(_.avatar_id -> None) .update(_.avatar_id -> None)
) )
} yield (deleted, updated) } yield (deleted, updated)
@ -455,6 +589,18 @@ object SessionOutfitHandlers {
query[Outfitmember].filter(_.outfit_id == lift(id)).size query[Outfitmember].filter(_.outfit_id == lift(id)).size
} }
def getOutfitMemberPoints(outfit_id: Long, avatar_id: Long): Future[Long] = {
val avatarOpt: Option[Long] = Some(avatar_id)
for {
points <- ctx.run(
query[Outfitpoint]
.filter(_.outfit_id == lift(outfit_id))
.filter(_.avatar_id == lift(avatarOpt))
.map(_.points)
)
} yield (points.headOption.getOrElse(0))
}
def getOutfitPoints(id: Long): Quoted[EntityQuery[OutfitpointMv]] = quote { def getOutfitPoints(id: Long): Quoted[EntityQuery[OutfitpointMv]] = quote {
querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(id)) querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(id))
} }
@ -490,4 +636,49 @@ object SessionOutfitHandlers {
(outfit.id, points.map(_.points).getOrElse(0L), outfit.name, leader.name, memberCounts.map(_._2).getOrElse(0L)) (outfit.id, points.map(_.points).getOrElse(0L), outfit.name, leader.name, memberCounts.map(_._2).getOrElse(0L))
} }
} }
def updateMemberRankById(outfit_id: Long, avatar_id: Long, rank: Int): Quoted[Update[Outfitmember]] = quote {
query[Outfitmember]
.filter(_.outfit_id == lift(outfit_id))
.filter(_.avatar_id == lift(avatar_id))
.update(_.rank -> lift(rank))
}
def updateOutfitMemberRank(outfit_id: Long, avatar_id: Long, rank: Int): Future[Unit] = {
ctx.transaction { implicit ec =>
for {
_ <- ctx.run(updateMemberRankById(outfit_id, avatar_id, rank))
} yield ()
}
}
def updateOutfitOwnerById(outfit_id: Long, owner_id: Long): Quoted[Update[Outfit]] = quote {
query[Outfit]
.filter(_.id == lift(outfit_id))
.update(_.owner_id -> lift(owner_id))
}
def updateOutfitOwner(outfit_id: Long, owner_id: Long, new_owner_id: Long): Future[Unit] = {
ctx.transaction { implicit ec =>
for {
_ <- ctx.run(updateMemberRankById(outfit_id, owner_id, 6))
_ <- ctx.run(updateMemberRankById(outfit_id, new_owner_id, 7))
_ <- ctx.run(updateOutfitOwnerById(outfit_id, new_owner_id))
} yield ()
}
}
def updateOutfitMotdById(outfit_id: Long, motd: Option[String]): Quoted[Update[Outfit]] = quote {
query[Outfit]
.filter(_.id == lift(outfit_id))
.update(_.motd -> lift(motd))
}
def updateOutfitMotd(outfit_id: Long, motd: String): Future[Unit] = {
ctx.transaction { implicit ec =>
for {
_ <- ctx.run(updateOutfitMotdById(outfit_id, Some(motd)))
} yield ()
}
}
} }

View file

@ -31,9 +31,9 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] {
type Type = Value type Type = Value
val CreateResponse: PacketType.Value = Value(0) val CreateResponse: PacketType.Value = Value(0)
val Invite: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player val Invite: PacketType.Value = Value(1) // response to OutfitMembershipRequest Unk2 for that player
val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added val InviteAccepted: PacketType.Value = Value(2)
val Unk3: PacketType.Value = Value(3) val InviteRejected: PacketType.Value = Value(3)
val Unk4: PacketType.Value = Value(4) val Unk4: PacketType.Value = Value(4)
val Kick: PacketType.Value = Value(5) val Kick: PacketType.Value = Value(5)
val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown

View file

@ -8,7 +8,7 @@ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
final case class OutfitRequest( final case class OutfitRequest(
id: Long, outfit_id: Long,
action: OutfitRequestAction action: OutfitRequestAction
) extends PlanetSideGamePacket { ) extends PlanetSideGamePacket {
type Packet = OutfitRequest type Packet = OutfitRequest

View file

@ -42,7 +42,7 @@ class OutfitMembershipResponseTest extends Specification {
"decode unk1" in { "decode unk1" in {
PacketCoding.decodePacket(unk1).require match { PacketCoding.decodePacket(unk1).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk1 packet_type mustEqual PacketType.Invite
unk0 mustEqual 0 unk0 mustEqual 0
unk1 mustEqual 0 unk1 mustEqual 0
outfit_id mustEqual 30383325 outfit_id mustEqual 30383325
@ -56,7 +56,7 @@ class OutfitMembershipResponseTest extends Specification {
} }
"encode unk1" in { "encode unk1" in {
val msg = OutfitMembershipResponse(PacketType.Unk1, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false) val msg = OutfitMembershipResponse(PacketType.Invite, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk1 pkt mustEqual unk1
@ -65,7 +65,7 @@ class OutfitMembershipResponseTest extends Specification {
"decode unk2" in { "decode unk2" in {
PacketCoding.decodePacket(unk2).require match { PacketCoding.decodePacket(unk2).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk2 packet_type mustEqual PacketType.InviteAccepted
unk0 mustEqual 0 unk0 mustEqual 0
unk1 mustEqual 0 unk1 mustEqual 0
outfit_id mustEqual 41605156 outfit_id mustEqual 41605156
@ -79,7 +79,7 @@ class OutfitMembershipResponseTest extends Specification {
} }
"encode unk2" in { "encode unk2" in {
val msg = OutfitMembershipResponse(PacketType.Unk2, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false) val msg = OutfitMembershipResponse(PacketType.InviteAccepted, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk2 pkt mustEqual unk2
@ -88,7 +88,7 @@ class OutfitMembershipResponseTest extends Specification {
"decode unk3" in { "decode unk3" in {
PacketCoding.decodePacket(unk3).require match { PacketCoding.decodePacket(unk3).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk3 packet_type mustEqual PacketType.InviteRejected
unk0 mustEqual 0 unk0 mustEqual 0
unk1 mustEqual 0 unk1 mustEqual 0
outfit_id mustEqual 41574772 outfit_id mustEqual 41574772
@ -102,7 +102,7 @@ class OutfitMembershipResponseTest extends Specification {
} }
"encode unk3" in { "encode unk3" in {
val msg = OutfitMembershipResponse(PacketType.Unk3, 0, 0, 41574772, 31156616, "", "", flag = true) val msg = OutfitMembershipResponse(PacketType.InviteRejected, 0, 0, 41574772, 31156616, "", "", flag = true)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk3 pkt mustEqual unk3
@ -134,7 +134,7 @@ class OutfitMembershipResponseTest extends Specification {
"decode unk5" in { "decode unk5" in {
PacketCoding.decodePacket(unk5).require match { PacketCoding.decodePacket(unk5).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk5 packet_type mustEqual PacketType.Kick
unk0 mustEqual 0 unk0 mustEqual 0
unk1 mustEqual 1 unk1 mustEqual 1
outfit_id mustEqual 41593365 outfit_id mustEqual 41593365
@ -148,7 +148,7 @@ class OutfitMembershipResponseTest extends Specification {
} }
"encode unk5" in { "encode unk5" in {
val msg = OutfitMembershipResponse(PacketType.Unk5, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true) val msg = OutfitMembershipResponse(PacketType.Kick, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk5 pkt mustEqual unk5