Merge branch 'even-more-outfit-packets-2' into outfit

This commit is contained in:
Resaec 2025-08-30 22:44:18 +02:00
commit 446dee8235
38 changed files with 2840 additions and 380 deletions

View file

@ -253,6 +253,8 @@ object AvatarActor {
final case class SupportExperienceDeposit(bep: Long, delay: Long) extends Command
case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Option[Long], points: Long)
/**
* A player loadout represents all of the items in the player's hands (equipment slots)
* and all of the items in the player's backpack (inventory)
@ -970,6 +972,22 @@ object AvatarActor {
}
}
def setOutfitPoints(avatarId: Long, exp: Long): Future[Unit] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
val avatarOpt: Option[Long] = Some(avatarId)
ctx.transaction { implicit ec =>
for {
currOp <- ctx.run(query[Outfitpoint].filter(_.avatar_id == lift(avatarOpt)).map(_.points))
.map(_.headOption.getOrElse(0L))
newOp = currOp + exp
_ <- ctx.run(query[Outfitpoint].filter(_.avatar_id == lift(avatarOpt)).update(_.points -> lift(newOp)))
} yield ()
}
}
def loadExperienceDebt(avatarId: Long): Future[Long] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
@ -2994,6 +3012,13 @@ class AvatarActor(
}
}
avatar = avatar.copy(bep = newBep, implants = implants)
if (player.outfit_id != 0) {
setOutfitPoints(player.avatar.id, bep).onComplete {
case Success(_) =>
case Failure(exception) => log.error(exception)("db failure")
}
}
case Failure(exception) =>
log.error(exception)("db failure")
}
@ -3010,6 +3035,12 @@ class AvatarActor(
zone.id,
AvatarAction.PlanetsideAttributeToAll(sess.player.GUID, 18, cep)
)
if (sess.player.outfit_id != 0) {
setOutfitPoints(sess.player.avatar.id, cep * 2).onComplete {
case Success(_) =>
case Failure(exception) => log.error(exception)("db failure")
}
}
case Failure(exception) =>
log.error(exception)("db failure")
}

View file

@ -12,7 +12,7 @@ import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage}
import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage}
import net.psforever.services.{InterstellarClusterService => ICS}
import net.psforever.services.CavernRotationService
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
@ -610,7 +610,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case packet: HitHint =>
logic.general.handleHitHint(packet)
case _: OutfitRequest => ()
case packet: OutfitRequest =>
logic.general.handleOutfitRequest(packet)
case packet: OutfitMembershipRequest =>
logic.general.handleOutfitMembershipRequest(packet)
case packet: OutfitMembershipResponse =>
logic.general.handleOutfitMembershipResponse(packet)
case pkt =>
data.log.warn(s"Unhandled GamePacket $pkt")

View file

@ -30,11 +30,13 @@ import net.psforever.objects.vehicles.Utility
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk0, Unk1}
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMemberEvent, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import scodec.bits.ByteVector
import scala.util.Success
@ -665,6 +667,19 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
val HitHint(_, _) = pkt
}
def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {}
def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {}
def handleOutfitRequest(pkt: OutfitRequest): Unit = {
pkt match {
case OutfitRequest(_, OutfitRequestAction.Unk3(true)) =>
case OutfitRequest(_, OutfitRequestAction.Unk3(false)) =>
case _ =>
}
}
/* messages */
def handleRenewCharSavedTimer(): Unit = { /* */ }

View file

@ -10,7 +10,7 @@ import net.psforever.objects.serverobject.containable.ContainableBehavior
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.interaction.Adversarial
import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction}
import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction, PlanetsideStringAttributeMessage}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.ImplantType
@ -252,6 +252,9 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget =>
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
case AvatarResponse.PlanetsideStringAttribute(attributeType, attributeValue) =>
sendResponse(PlanetsideStringAttributeMessage(guid, attributeType, attributeValue))
case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
sendResponse(GenericObjectActionMessage(objectGuid, actionCode))

View file

@ -7,7 +7,7 @@ import net.psforever.actors.session.spectator.SpectatorMode
import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
import net.psforever.objects.Session
import net.psforever.packet.game.{ChatMsg, ServerType, SetChatFilterMessage}
import net.psforever.services.chat.{DefaultChannel, SquadChannel}
import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel}
import net.psforever.types.ChatMessageType
import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM}
import net.psforever.util.Config
@ -79,6 +79,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_SQUAD, _, _) =>
ops.commandSquad(session, message, SquadChannel(sessionLogic.squad.squad_guid))
case (CMT_OUTFIT, _, _) =>
ops.commandOutfit(session, message, OutfitChannel(sessionLogic.player.outfit_id))
case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
ops.commandWho(session)
@ -100,7 +103,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
import ChatMessageType._
message.messageType match {
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE | CMT_OUTFIT =>
ops.commandIncomingSendAllIfOnline(session, message)
case CMT_OPEN =>

View file

@ -4,7 +4,7 @@ package net.psforever.actors.session.normal
import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData, SessionOutfitHandlers}
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry}
import net.psforever.objects.ballistics.Projectile
@ -37,13 +37,16 @@ import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk2}
import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMembershipRequest, OutfitMembershipRequestAction, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.chat.OutfitChannel
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.local.support.CaptureFlagManager
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.Config
import net.psforever.zones.Zones.zones
import scala.concurrent.duration._
@ -796,6 +799,56 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
val HitHint(_, _) = pkt
}
def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {
pkt match {
case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Form(_, outfitName)) =>
if (player.outfit_id == 0) {
SessionOutfitHandlers.HandleOutfitForm(outfitName, player, sessionLogic)
}
case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Invite(_, invitedName)) =>
SessionOutfitHandlers.HandleOutfitInvite(zones, invitedName, player)
case OutfitMembershipRequest(_, OutfitMembershipRequestAction.AcceptInvite(_)) =>
SessionOutfitHandlers.HandleOutfitInviteAccept(player, sessionLogic)
case OutfitMembershipRequest(_, OutfitMembershipRequestAction.RejectInvite(_)) =>
SessionOutfitHandlers.HandleOutfitInviteReject(player)
case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Kick(memberId, _)) =>
SessionOutfitHandlers.HandleOutfitKick(zones, memberId, player, sessionLogic)
case OutfitMembershipRequest(_, OutfitMembershipRequestAction.SetRank(memberId, newRank, _)) =>
SessionOutfitHandlers.HandleOutfitPromote(zones, memberId, newRank, player)
case _ =>
}
}
def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {}
def handleOutfitRequest(pkt: OutfitRequest): Unit = {
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))) =>
// 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))))
case OutfitRequest(_, OutfitRequestAction.Unk3(true)) =>
SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id)
case OutfitRequest(_, OutfitRequestAction.Unk3(false)) =>
case OutfitRequest(_, OutfitRequestAction.Unk4(true)) =>
SessionOutfitHandlers.HandleGetOutfitList(player)
case _ =>
}
}
/* messages */
def handleRenewCharSavedTimer(): Unit = {

View file

@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.vehicles.Utility
import net.psforever.objects.zones.ZoneProjectile
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.account.AccountPersistenceService
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{ExoSuitType, Vector3}
@ -375,6 +375,12 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ }
def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {}
def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {}
def handleOutfitRequest(pkt: OutfitRequest): Unit = {}
/* messages */
def handleRenewCharSavedTimer(): Unit = { /* intentionally blank */ }

View file

@ -14,7 +14,7 @@ import net.psforever.objects.LivePlayerList
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.ZoneInfo
import net.psforever.packet.game.SetChatFilterMessage
import net.psforever.services.chat.{DefaultChannel, SquadChannel}
import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse}
import net.psforever.types.ChatMessageType.CMT_QUIT
@ -446,6 +446,14 @@ class ChatOperations(
}
}
def commandOutfit(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
channels.foreach {
case _/*channel*/: OutfitChannel =>
commandSendToRecipient(session, message, toChannel)
case _ => ()
}
}
def commandWho(session: Session): Unit = {
val players = session.zone.Players
val popTR = players.count(_.faction == PlanetSideEmpire.TR)

View file

@ -166,6 +166,12 @@ trait GeneralFunctions extends CommonSessionInterfacingFunctionality {
def handleCanNotPutItemInSlot(msg: Containable.CanNotPutItemInSlot): Unit
def handleReceiveDefaultMessage(default: Any, sender: ActorRef): Unit
def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit
def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit
def handleOutfitRequest(pkt: OutfitRequest): Unit
}
class GeneralOperations(

View file

@ -0,0 +1,34 @@
package net.psforever.actors.session.support
import net.psforever.objects.Player
import scala.collection.mutable
case class OutfitInvite(
sentTo: Player,
sentFrom: Player,
timestamp: Long = System.currentTimeMillis() / 1000
)
object OutfitInviteManager {
private val invites = mutable.Map[Long, OutfitInvite]()
private val ExpirationSeconds = 320
def addOutfitInvite(invite: OutfitInvite): Boolean = {
invites.get(invite.sentTo.CharId) match {
case Some(existing) if (System.currentTimeMillis() / 1000 - existing.timestamp) < ExpirationSeconds =>
false // Reject new invite (previous one is still valid)
case _ =>
invites(invite.sentTo.CharId) = invite
true
}
}
def removeOutfitInvite(sentToId: Long): Unit = {
invites.remove(sentToId)
}
def getOutfitInvite(sentToId: Long): Option[OutfitInvite] = invites.get(sentToId)
def getAllOutfitInvites: List[OutfitInvite] = invites.values.toList
}

View file

@ -0,0 +1,732 @@
// Copyright (c) 2025 PSForever
package net.psforever.actors.session.support
import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase, Update}
import net.psforever.objects.avatar.PlayerControl
import net.psforever.objects.zones.Zone
import net.psforever.objects.Player
import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk0, Unk1, Unk2}
import net.psforever.packet.game.OutfitMembershipResponse.PacketType.CreateResponse
import net.psforever.packet.game._
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.chat.OutfitChannel
import net.psforever.types.ChatMessageType
import net.psforever.util.Config
import java.time.LocalDateTime
import scala.util.{Failure, Success}
object SessionOutfitHandlers {
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,
deleted: Boolean,
rank0: Option[String],
rank1: Option[String],
rank2: Option[String],
rank3: Option[String],
rank4: Option[String],
rank5: Option[String],
rank6: Option[String],
rank7: Option[String]
)
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 OutfitpointMv(outfit_id: Long, points: Long)
val ctx = new PostgresJAsyncContext(SnakeCase, Config.config.getConfig("database"))
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def HandleOutfitForm(outfitName: String, player: Player, session: SessionData): Unit = {
val cleanedName = sanitizeOutfitName(outfitName)
cleanedName match {
case Some(validName) =>
ctx.run(findOutfitByName(validName)).flatMap {
case existing if existing.nonEmpty =>
PlayerControl.sendResponse(player.Zone, player.Name,
ChatMsg(ChatMessageType.UNK_227, "@OutfitErrorNameAlreadyTaken"))
Future.successful(())
case _ =>
createNewOutfit(validName, player.Faction.id, player.CharId).map { outfit =>
val seconds: Long =
outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitEvent(outfit.id, Unk2(
OutfitInfo(
outfit.name, 0, 0, 1,
OutfitRankNames("", "", "", "", "", "", "", ""),
"",
14, unk11 = true, 0, seconds, 0, 0, 0))))
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitMemberUpdate(outfit.id, player.CharId, 7, flag = true))
PlayerControl.sendResponse(player.Zone, player.Name,
ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateSuccess"))
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitMembershipResponse(CreateResponse, 0, 0, player.CharId, 0, "", "", flag = true))
player.outfit_id = outfit.id
player.outfit_name = outfit.name
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id,
AvatarAction.PlanetsideAttributeToAll(player.GUID, 39, outfit.id))
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id,
AvatarAction.PlanetsideStringAttribute(player.GUID, 0, outfit.name))
session.chat.JoinChannel(OutfitChannel(player.outfit_id))
}
.recover { case e =>
e.printStackTrace()
PlayerControl.sendResponse(player.Zone, player.Name,
ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateFailure"))
}
}
case None =>
PlayerControl.sendResponse(player.Zone, player.Name,
ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateFailure"))
}
}
def HandleOutfitInvite(zones: Seq[Zone], invitedName: String, sentFrom: Player): Unit = {
findPlayerByNameForOutfitAction(zones, invitedName, sentFrom).foreach { invitedPlayer =>
PlayerControl.sendResponse(invitedPlayer.Zone, invitedPlayer.Name,
OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Invite, 0, 0,
sentFrom.CharId, sentFrom.CharId, sentFrom.Name, sentFrom.outfit_name, flag = false))
PlayerControl.sendResponse(sentFrom.Zone, sentFrom.Name,
OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Invite, 0, 0,
sentFrom.CharId, invitedPlayer.CharId, invitedPlayer.Name, sentFrom.outfit_name, flag = true))
val outfitInvite = OutfitInvite(invitedPlayer, sentFrom)
OutfitInviteManager.addOutfitInvite(outfitInvite)
}
}
def HandleOutfitInviteAccept(invited: Player, session: SessionData): Unit = {
OutfitInviteManager.getOutfitInvite(invited.CharId) match {
case Some(outfitInvite) =>
val outfitId = outfitInvite.sentFrom.outfit_id
(for {
_ <- addMemberToOutfit(outfitId, invited.CharId)
outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption)
memberCount <- ctx.run(getOutfitMemberCount(outfitId))
points <- ctx.run(getOutfitPoints(outfitId)).map(_.headOption.map(_.points).getOrElse(0L))
} yield (outfitOpt, memberCount, points))
.map {
case (Some(outfit), memberCount, points) =>
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.InviteAccepted, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = false))
PlayerControl.sendResponse(invited.Zone, invited.Name,
OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.InviteAccepted, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = true))
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
OutfitEvent(outfitId, OutfitEventAction.Unk5(memberCount)))
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
OutfitMemberEvent(outfitId, invited.CharId,
OutfitMemberEventAction.Unk0(invited.Name, 0, 0, 0,
OutfitMemberEventAction.PacketType.Padding, 0)))
val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000
PlayerControl.sendResponse(invited.Zone, invited.Name,
OutfitEvent(outfitId, Unk0(OutfitInfo(
outfit.name, points, points, memberCount,
OutfitRankNames("", "", "", "", "", "", "", ""),
outfit.motd.getOrElse(""),
14, unk11 = true, 0, seconds, 0, 0, 0))))
PlayerControl.sendResponse(invited.Zone, invited.Name,
OutfitMemberUpdate(outfit.id, invited.CharId, 0, flag=true))
OutfitInviteManager.removeOutfitInvite(invited.CharId)
session.chat.JoinChannel(OutfitChannel(outfit.id))
invited.outfit_id = outfit.id
invited.outfit_name = outfit.name
invited.Zone.AvatarEvents ! AvatarServiceMessage(invited.Zone.id,
AvatarAction.PlanetsideAttributeToAll(invited.GUID, 39, outfit.id))
invited.Zone.AvatarEvents ! AvatarServiceMessage(invited.Zone.id,
AvatarAction.PlanetsideStringAttribute(invited.GUID, 0, outfit.name))
case (None, _, _) =>
PlayerControl.sendResponse(invited.Zone, invited.Name,
ChatMsg(ChatMessageType.UNK_227, "Failed to join outfit"))
}
.recover { case _ =>
PlayerControl.sendResponse(invited.Zone, invited.Name,
ChatMsg(ChatMessageType.UNK_227, "Failed to join outfit"))
}
case None =>
}
}
def HandleOutfitInviteReject(invited: Player): Unit = {
OutfitInviteManager.getOutfitInvite(invited.CharId) match {
case Some(outfitInvite) =>
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.InviteRejected, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = false))
PlayerControl.sendResponse(invited.Zone, invited.Name,
OutfitMembershipResponse(
OutfitMembershipResponse.PacketType.InviteRejected, 0, 0,
invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = true))
OutfitInviteManager.removeOutfitInvite(invited.CharId)
case None =>
}
}
def HandleOutfitKick(zones: Seq[Zone], kickedId: Long, kickedBy: Player, session: SessionData): Unit = {
// if same id, player has left the outfit by their own choice
if (kickedId == kickedBy.CharId) {
removeMemberFromOutfit(kickedBy.outfit_id, kickedId).map {
case (deleted, _) =>
if (deleted > 0) {
PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name,
OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1()))
zones.filter(z => z.AllPlayers.nonEmpty).flatMap(_.AllPlayers)
.filter(p => p.outfit_id == kickedBy.outfit_id).foreach(outfitMember =>
PlayerControl.sendResponse(outfitMember.Zone, outfitMember.Name,
OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1()))
)
session.chat.LeaveChannel(OutfitChannel(kickedBy.outfit_id))
kickedBy.outfit_name = ""
kickedBy.outfit_id = 0
kickedBy.Zone.AvatarEvents ! AvatarServiceMessage(kickedBy.Zone.id,
AvatarAction.PlanetsideAttributeToAll(kickedBy.GUID, 39, 0))
kickedBy.Zone.AvatarEvents ! AvatarServiceMessage(kickedBy.Zone.id,
AvatarAction.PlanetsideStringAttribute(kickedBy.GUID, 0, ""))
}
}.recover { case e =>
e.printStackTrace()
}
}
else {
removeMemberFromOutfit(kickedBy.outfit_id, kickedId).map {
case (deleted, _) =>
if (deleted > 0) {
findPlayerByIdForOutfitAction(zones, kickedId, kickedBy).foreach { kicked =>
PlayerControl.sendResponse(kicked.Zone, kicked.Name,
OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1,
kickedBy.CharId, kicked.CharId, kicked.Name, kickedBy.Name, flag = false))
kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id,
AvatarAction.PlanetsideAttributeToAll(kicked.GUID, 39, 0))
kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id,
AvatarAction.PlanetsideStringAttribute(kicked.GUID, 0, ""))
kicked.outfit_id = 0
kicked.outfit_name = ""
PlayerControl.sendResponse(kicked.Zone, kicked.Name,
OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1()))
}
val avatarName: Future[Option[String]] =
ctx.run(
quote { query[Avatar].filter(_.id == lift(kickedId)).map(_.name) }
).map(_.headOption)
avatarName.foreach {
case Some(name) => PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name,
OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kickedId, name, "", flag = true))
case None => PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name,
OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kickedId, "NameNotFound", "", flag = true))
}
zones.filter(z => z.AllPlayers.nonEmpty).flatMap(_.AllPlayers)
.filter(p => p.outfit_id == kickedBy.outfit_id).foreach(outfitMember =>
PlayerControl.sendResponse(outfitMember.Zone, outfitMember.Name,
OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1()))
)
// this needs to be the kicked player
// session.chat.LeaveChannel(OutfitChannel(kickedBy.outfit_id))
// new number of outfit members?
//PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitEvent(kickedBy.outfit_id, OutfitEventAction.Unk5(34)))
}
}.recover { case e =>
e.printStackTrace()
}
}
}
def HandleOutfitPromote(zones: Seq[Zone], promotedId: Long, newRank: Int, promoter: Player): Unit = {
val outfit_id = promoter.outfit_id
findPlayerByIdForOutfitAction(zones, promotedId, promoter).foreach { promoted =>
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))
}
}
def HandleViewOutfitWindow(zones: Seq[Zone], player: Player, outfitId: Long): Unit = {
val outfitDetailsF = for {
outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption)
memberCount <- ctx.run(query[Outfitmember].filter(_.outfit_id == lift(outfitId)).size)
pointsTotal <- ctx.run(querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(outfitId)))
} yield (outfitOpt, memberCount, pointsTotal.headOption.map(_.points).getOrElse(0L))
val membersF = ctx.run(getOutfitMembersWithDetails(outfitId))
for {
(outfitOpt, memberCount, totalPoints) <- outfitDetailsF
members <- membersF
} yield {
outfitOpt.foreach { outfit =>
val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitEvent(outfit.id, Unk0(OutfitInfo(
outfit.name,
totalPoints,
totalPoints,
memberCount,
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(""),
14, unk11 = true, 0, seconds, 0, 0, 0))))
members.foreach { case (avatarId, avatarName, points, rank, login) =>
val lastLogin = findPlayerByIdForOutfitAction(zones, avatarId, player) match {
case Some(_) => 0L
case None if player.Name == avatarName => 0L
case None => (System.currentTimeMillis() - login.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli) / 1000
}
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitMemberEvent(outfit.id, avatarId,
OutfitMemberEventAction.Unk0(
avatarName,
rank,
points,
lastLogin,
OutfitMemberEventAction.PacketType.Padding, 0)))
}
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitEvent(outfit.id, Unk1()))
}
}
}
def HandleGetOutfitList(player: Player): Unit = {
val q = getOutfitsByEmpire(player.Faction.id)
val futureResult = ctx.run(q)
futureResult.onComplete {
case Success(rows) =>
rows.foreach { case (outfitId, points, name, leaderName, memberCount) =>
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitListEvent(
OutfitListEventAction.ListElementOutfit(
outfitId,
points,
memberCount,
name,
leaderName)))
}
case Failure(_) =>
PlayerControl.sendResponse(player.Zone, player.Name,
ChatMsg(ChatMessageType.UNK_227, "Outfit list failed to return")
)
}
}
def HandleOutfitMotd(zones: Seq[Zone], message: String, player: Player): Unit = {
val outfit_id = player.outfit_id
val outfitDetails = for {
_ <- updateOutfitMotd(outfit_id, message)
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)))
}
def HandleLoginOutfitCheck(player: Player, session: SessionData): Unit = {
ctx.run(getOutfitOnLogin(player.avatar.id)).flatMap { memberships =>
memberships.headOption match {
case Some(membership) =>
val outfitId = membership.outfit_id
(for {
outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption)
memberCount <- ctx.run(getOutfitMemberCount(outfitId))
points <- ctx.run(getOutfitPoints(outfitId)).map(_.headOption.map(_.points).getOrElse(0L))
} yield (outfitOpt, memberCount, points))
.map {
case (Some(outfit), memberCount, points) =>
val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitEvent(outfitId, Unk2(OutfitInfo(
outfit.name, points, points, memberCount,
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(""),
14, unk11 = true, 0, seconds, 0, 0, 0))))
PlayerControl.sendResponse(player.Zone, player.Name,
OutfitMemberUpdate(outfit.id, player.CharId, membership.rank, flag = true))
session.chat.JoinChannel(OutfitChannel(outfit.id))
player.outfit_id = outfit.id
player.outfit_name = outfit.name
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id,
AvatarAction.PlanetsideAttributeToAll(player.GUID, 39, outfit.id))
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id,
AvatarAction.PlanetsideStringAttribute(player.GUID, 0, outfit.name))
case (None, _, _) =>
PlayerControl.sendResponse(player.Zone, player.Name,
ChatMsg(ChatMessageType.UNK_227, "Failed to load outfit"))
}
.recover { case _ =>
PlayerControl.sendResponse(player.Zone, player.Name,
ChatMsg(ChatMessageType.UNK_227, "Failed to load outfit"))
}
case None =>
Future.successful(())
}
}
}
/* supporting functions */
def sanitizeOutfitName(name: String): Option[String] = {
val cleaned = name
.replaceAll("""[^A-Za-z0-9\-="\;\[\]\(\)\. ]""", "") // Remove disallowed chars
.replaceAll(" +", " ") // Collapse multiple spaces to one
.trim // Remove leading/trailing spaces
if (cleaned.length >= 2 && cleaned.length <= 32) Some(cleaned) else None
}
def findPlayerByNameForOutfitAction(zones: Iterable[Zone], targetName: String, inviter: Player): Option[Player] = {
zones
.flatMap(_.AllPlayers)
.find(p =>
p.Name.equalsIgnoreCase(targetName) && p.Name != inviter.Name &&
p.Faction == inviter.Faction && p.outfit_id == 0
)
}
def findPlayerByIdForOutfitAction(zones: Iterable[Zone], targetId: Long, initiator: Player): Option[Player] = {
zones
.flatMap(_.AllPlayers)
.find(p =>
p.CharId == targetId && p.Name != initiator.Name &&
p.Faction == initiator.Faction && p.outfit_id == initiator.outfit_id
)
}
/* db actions */
def findOutfitByName(name: String): Quoted[EntityQuery[Outfit]] = quote {
query[Outfit].filter(outfit => lift(name).toLowerCase == outfit.name.toLowerCase)
}
def insertNewOutfit(name: String, faction: Int, owner_id: Long): Quoted[ActionReturning[Outfit, Outfit]] = quote {
query[Outfit]
.insert(_.name -> lift(name), _.faction -> lift(faction), _.owner_id -> lift(owner_id))
.returning(outfit => outfit)
}
def insertOutfitMember(outfit_id: Long, avatar_id: Long, rank: Int): Quoted[Insert[Outfitmember]] = quote {
query[Outfitmember].insert(
_.outfit_id -> lift(outfit_id),
_.avatar_id -> lift(avatar_id),
_.rank -> lift(rank)
)
}
def insertOutfitPoint(outfit_id: Long, avatar_id: Long): Quoted[Insert[Outfitpoint]] = quote {
query[Outfitpoint].insert(
_.outfit_id -> lift(outfit_id),
_.avatar_id -> lift(Some(avatar_id): Option[Long])
)
}
def createNewOutfit(name: String, faction: Int, owner_id: Long): Future[Outfit] = {
ctx.transaction { implicit ec =>
for {
outfit <- ctx.run(insertNewOutfit(name, faction, owner_id))
_ <- ctx.run(insertOutfitMember(outfit.id, owner_id, rank=7))
_ <- ctx.run(insertOutfitPoint(outfit.id, owner_id))
} yield outfit
}
}
def addMemberToOutfit(outfit_id: Long, avatar_id: Long): Future[Unit] = {
ctx.transaction { implicit ec =>
for {
_ <- ctx.run(insertOutfitMember(outfit_id, avatar_id, rank=0))
_ <- ctx.run(insertOutfitPoint(outfit_id, avatar_id))
} yield ()
}
}
def removeMemberFromOutfit(outfit_id: Long, avatar_id: Long): Future[(Long, Long)] = {
val avatarOpt: Option[Long] = Some(avatar_id)
ctx.transaction { _ =>
for {
deleted <- ctx.run(
query[Outfitmember]
.filter(_.outfit_id == lift(outfit_id))
.filter(_.avatar_id == lift(avatar_id))
.delete
)
updated <- ctx.run(
query[Outfitpoint]
.filter(_.outfit_id == lift(outfit_id))
.filter(_.avatar_id == lift(avatarOpt))
.update(_.avatar_id -> None)
)
} yield (deleted, updated)
}
}
def getOutfitById(id: Long): Quoted[EntityQuery[Outfit]] = quote {
query[Outfit].filter(_.id == lift(id))
}
def getOutfitMemberCount(id: Long): Quoted[Long] = quote {
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 {
querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(id))
}
def getOutfitOnLogin(avatarId: Long): Quoted[EntityQuery[Outfitmember]] = quote {
query[Outfitmember].filter(_.avatar_id == lift(avatarId))
}
def getOutfitMembersWithDetails(outfitId: Long): Quoted[Query[(Long, String, Long, Int, LocalDateTime)]] = quote {
query[Outfitmember]
.filter(_.outfit_id == lift(outfitId))
.join(query[Avatar]).on(_.avatar_id == _.id)
.leftJoin(query[Outfitpoint]).on {
case ((member, _), points) =>
points.outfit_id == member.outfit_id && points.avatar_id.getOrElse(0L) == member.avatar_id
}
.map {
case ((member, avatar), pointsOpt) =>
(member.avatar_id, avatar.name, pointsOpt.map(_.points).getOrElse(0L), member.rank, avatar.last_login)
}
}
def getOutfitsByEmpire(playerEmpireId: Int): Quoted[Query[(Long, Long, String, String, Long)]] = quote {
query[Outfit]
.filter(_.faction == lift(playerEmpireId))
.join(query[Avatar]).on((outfit, avatar) => outfit.owner_id == avatar.id)
.leftJoin(
query[Outfitmember]
.groupBy(_.outfit_id)
.map { case (oid, members) => (oid, members.size) }
).on { case ((outfit, _), (oid, _)) => oid == outfit.id }
.leftJoin(querySchema[OutfitpointMv]("outfitpoint_mv")).on {
case (((outfit, _), _), points) => points.outfit_id == outfit.id
}
.map {
case (((outfit, leader), memberCounts), points) =>
(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

@ -2532,6 +2532,7 @@ class ZoningOperations(
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
}
}
SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic)
//make weather happen
sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
@ -2655,6 +2656,7 @@ class ZoningOperations(
log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data")
}
setupAvatarFunc = AvatarCreate
SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic)
//make weather happen
sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
@ -3196,8 +3198,6 @@ class ZoningOperations(
continent.AllPlayers.filter(_.ExoSuit == ExoSuitType.MAX).foreach(max => sendResponse(PlanetsideAttributeMessage(max.GUID, 4, max.Armor)))
// AvatarAwardMessage
//populateAvatarAwardRibbonsFunc(1, 20L)
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
//squad stuff (loadouts, assignment)
sessionLogic.squad.squadSetup()
//MapObjectStateBlockMessage and ObjectCreateMessage?

View file

@ -85,6 +85,8 @@ class Player(var avatar: Avatar)
var silenced: Boolean = false
var death_by: Int = 0
var lastShotSeq_time: Int = -1
var outfit_name: String = ""
var outfit_id: Long = 0
/** From PlanetsideAttributeMessage */
var PlanetsideAttribute: Array[Long] = Array.ofDim(120)
@ -646,6 +648,8 @@ object Player {
obj.silenced = player.silenced
obj.allowInteraction = player.allowInteraction
obj.avatar.scorecard.respawn()
obj.outfit_name = player.outfit_name
obj.outfit_id = player.outfit_id
obj
} else {
player

View file

@ -93,8 +93,8 @@ object AvatarConverter {
0
)
val ab: (Boolean, Int) => CharacterAppearanceB = CharacterAppearanceB(
0L,
outfit_name = "",
obj.outfit_id,
obj.outfit_name,
outfit_logo = 0,
unk1 = false,
obj.isBackpack,

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017 PSForever
// Copyright (c) 2017-2025 PSForever
package net.psforever.packet
import scodec.{Attempt, Codec, DecodeResult, Err}
@ -469,13 +469,13 @@ object GamePacketOpcode extends Enumeration {
case 0x8a => game.PlayerStasisMessage.decode
case 0x8b => noDecoder(UnknownMessage139)
case 0x8c => game.OutfitMembershipRequest.decode
case 0x8d => noDecoder(OutfitMembershipResponse)
case 0x8d => game.OutfitMembershipResponse.decode
case 0x8e => game.OutfitRequest.decode
case 0x8f => noDecoder(OutfitEvent)
case 0x8f => game.OutfitEvent.decode
// OPCODES 0x90-9f
case 0x90 => noDecoder(OutfitMemberEvent)
case 0x91 => noDecoder(OutfitMemberUpdate)
case 0x90 => game.OutfitMemberEvent.decode
case 0x91 => game.OutfitMemberUpdate.decode
case 0x92 => game.PlanetsideStringAttributeMessage.decode
case 0x93 => game.DataChallengeMessage.decode
case 0x94 => game.DataChallengeMessageResp.decode
@ -483,7 +483,7 @@ object GamePacketOpcode extends Enumeration {
case 0x96 => game.SimDataChallenge.decode
case 0x97 => game.SimDataChallengeResp.decode
// 0x98
case 0x98 => noDecoder(OutfitListEvent)
case 0x98 => game.OutfitListEvent.decode
case 0x99 => noDecoder(EmpireIncentivesMessage)
case 0x9a => game.InvalidTerrainMessage.decode
case 0x9b => noDecoder(SyncMessage)

View file

@ -0,0 +1,263 @@
// Copyright (c) 2025 PSForever
package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.{Attempt, Codec, Err}
import scodec.bits.BitVector
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitEvent(
outfit_id: Long,
action: OutfitEventAction
) extends PlanetSideGamePacket {
type Packet = OutfitEvent
def opcode: Type = GamePacketOpcode.OutfitEvent
def encode: Attempt[BitVector] = OutfitEvent.encode(this)
}
abstract class OutfitEventAction(val code: Int)
object OutfitEventAction {
final case class OutfitRankNames(
rank1: String,
rank2: String,
rank3: String,
rank4: String,
rank5: String,
rank6: String,
rank7: String,
rank8: String,
)
final case class OutfitInfo(
outfit_name: String,
outfit_points1: Long,
outfit_points2: Long, // same as outfit_points1
member_count: Long,
outfit_rank_names: OutfitRankNames,
motd: String,
unk10: Int,
unk11: Boolean,
unk12: Long, // only set if unk11 is false
created_timestamp: Long,
unk23: Long,
unk24: Long,
unk25: Long,
)
final case class Unk0(
outfit_info: OutfitInfo
) extends OutfitEventAction(code = 0)
final case class Unk1(
) extends OutfitEventAction(code = 1)
final case class Unk2(
outfit_info: OutfitInfo,
) extends OutfitEventAction(code = 2)
final case class Unk3(
) extends OutfitEventAction(code = 3)
final case class UpdateOutfitId(
new_outfit_id: Long,
) extends OutfitEventAction(code = 4)
final case class Unk5(
unk1: Long,
) extends OutfitEventAction(code = 5)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitEventAction(badCode)
/**
* The `Codec`s used to transform the input stream into the context of a specific action
* and extract the field data from that stream.
*/
object Codecs {
private val everFailCondition = conditional(included = false, bool)
private val OutfitRankNamesCodec: Codec[OutfitRankNames] = (
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString
).xmap[OutfitRankNames](
{
case u0 :: u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil =>
OutfitRankNames(u0, u1, u2, u3, u4, u5, u6, u7)
},
{
case OutfitRankNames(u0, u1, u2, u3, u4, u5, u6, u7) =>
u0 :: u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil
}
)
private val InfoCodec: Codec[OutfitInfo] = (
("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) ::
("outfit_points1" | uint32L) ::
("outfit_points2" | uint32L) ::
("member_count" | uint32L) ::
("outfit_rank_names" | OutfitRankNamesCodec) ::
("motd" | PacketHelpers.encodedWideString) ::
("" | uint8L) ::
("" | bool) ::
("" | uint32L) ::
("created_timestamp" | uint32L) ::
("" | uint32L) ::
("" | uint32L) ::
("" | uint32L)
).xmap[OutfitInfo](
{
case outfit_name :: outfit_points1 :: outfit_points2 :: member_count :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: created_timestamp :: u23 :: u24 :: u25 :: HNil =>
OutfitInfo(outfit_name, outfit_points1, outfit_points2, member_count, outfit_rank_names, motd, u10, u11, u12, created_timestamp, u23, u24, u25)
},
{
case OutfitInfo(outfit_name, outfit_points1, outfit_points2, member_count, outfit_rank_names, motd, u10, u11, u12, created_timestamp, u23, u24, u25) =>
outfit_name :: outfit_points1 :: outfit_points2 :: member_count :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: created_timestamp :: u23 :: u24 :: u25 :: HNil
}
)
val Unk0Codec: Codec[Unk0] = (
("outfit_info" | InfoCodec)
).xmap[Unk0](
{
case info =>
Unk0(info)
},
{
case Unk0(info) =>
info
}
)
val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1())
val Unk2Codec: Codec[Unk2] = (
("outfit_info" | InfoCodec)
).xmap[Unk2](
{
case info =>
Unk2(info)
},
{
case Unk2(info) =>
info
}
)
val Unk3Codec: Codec[Unk3] = PacketHelpers.emptyCodec(Unk3())
val UpdateOutfitIdCodec: Codec[UpdateOutfitId] = ( // update outfit_id? // 2016.03.18 #10640 // after this packet the referenced id changes to the new one, old is not used again
("new_outfit_id" | uint32L)
).xmap[UpdateOutfitId](
{
case new_outfit_id =>
UpdateOutfitId(new_outfit_id)
},
{
case UpdateOutfitId(new_outfit_id) =>
new_outfit_id
}
)
val Unk5Codec: Codec[Unk5] = (
("" | uint32L)
).xmap[Unk5](
{
case u1 =>
Unk5(u1)
},
{
case Unk5(u1) =>
u1
}
)
/**
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
*
* @param action the action behavior code
* @return a transformation between the action code and the unknown bit data
*/
def unknownCodec(action: Int): Codec[Unknown] =
bits.xmap[Unknown](
data => Unknown(action, data),
{
case Unknown(_, data) => data
}
)
/**
* The action code was completely unanticipated!
*
* @param action the action behavior code
* @return nothing; always fail
*/
def failureCodec(action: Int): Codec[OutfitEventAction] =
everFailCondition.exmap[OutfitEventAction](
_ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
)
}
}
object OutfitEvent extends Marshallable[OutfitEvent] {
object PacketType extends Enumeration {
type Type = Value
val Unk0: PacketType.Value = Value(0) // start listing of members
val Unk1: PacketType.Value = Value(1) // end listing of members
val Unk2: PacketType.Value = Value(2) // send after creating an outfit // normal info, same as Unk0
val Unk3: PacketType.Value = Value(3) // below
val UpdateOutfitId: PacketType.Value = Value(4)
val Unk5: PacketType.Value = Value(5)
val Unk6: PacketType.Value = Value(6)
val Unk7: PacketType.Value = Value(7)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
private def selectFromType(code: Int): Codec[OutfitEventAction] = {
import OutfitEventAction.Codecs._
import scala.annotation.switch
((code: @switch) match {
case 0 => Unk0Codec // view outfit window and members
case 1 => Unk1Codec
case 2 => Unk2Codec // sent after /outfitcreate and on login if in an outfit
case 3 => Unk3Codec
case 4 => UpdateOutfitIdCodec
case 5 => Unk5Codec
case 6 => unknownCodec(action = code)
case 7 => unknownCodec(action = code)
case _ => failureCodec(code)
}).asInstanceOf[Codec[OutfitEventAction]]
}
implicit val codec: Codec[OutfitEvent] = (
("packet_type" | PacketType.codec) >>:~ { packet_type =>
("outfit_guid" | uint32L) ::
("action" | selectFromType(packet_type.id))
}
).xmap[OutfitEvent](
{
case _ :: outfit_guid :: action :: HNil =>
OutfitEvent(outfit_guid, action)
},
{
case OutfitEvent(outfit_guid, action) =>
OutfitEvent.PacketType(action.code) :: outfit_guid :: action :: HNil
}
)
}

View file

@ -0,0 +1,158 @@
// Copyright (c) 2025 PSForever
package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.{Attempt, Codec, Err}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitListEvent(
action: OutfitListEventAction
) extends PlanetSideGamePacket {
type Packet = OutfitListEvent
def opcode: Type = GamePacketOpcode.OutfitListEvent
def encode: Attempt[BitVector] = OutfitListEvent.encode(this)
}
abstract class OutfitListEventAction(val code: Int)
object OutfitListEventAction {
final case class ListElementOutfit(
outfit_id: Long,
points: Long,
members: Long,
outfit_name: String,
outfit_leader: String,
) extends OutfitListEventAction(code = 2)
/*
TODO: Check packet when bundle packet has been implemented (packet containing OutfitListEvent packets back to back)
For now it seems like there is no valid packet captured
*/
final case class Unk3(
unk1: Long,
) extends OutfitListEventAction(code = 3)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitListEventAction(badCode)
/**
* The `Codec`s used to transform the input stream into the context of a specific action
* and extract the field data from that stream.
*/
object Codecs {
private val everFailCondition = conditional(included = false, bool)
val ListElementOutfitCodec: Codec[ListElementOutfit] = (
("unk1" | uint32L) ::
("points" | uint32L) ::
("members" | uint32L) ::
("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) ::
("outfit_leader" | PacketHelpers.encodedWideString)
).xmap[ListElementOutfit](
{
case u1 :: points :: members :: outfit_name :: outfit_leader :: HNil =>
ListElementOutfit(u1, points, members, outfit_name, outfit_leader)
},
{
case ListElementOutfit(u1, points, members, outfit_name, outfit_leader) =>
u1 :: points :: members :: outfit_name :: outfit_leader :: HNil
}
)
val Unk3Codec: Codec[Unk3] = (
("unk1" | uint32L)
).xmap[Unk3](
{
case u1 =>
Unk3(u1)
},
{
case Unk3(u1) =>
u1
}
)
/**
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
*
* @param action the action behavior code
* @return a transformation between the action code and the unknown bit data
*/
def unknownCodec(action: Int): Codec[Unknown] =
bits.xmap[Unknown](
data => Unknown(action, data),
{
case Unknown(_, data) => data
}
)
/**
* The action code was completely unanticipated!
*
* @param action the action behavior code
* @return nothing; always fail
*/
def failureCodec(action: Int): Codec[OutfitListEventAction] =
everFailCondition.exmap[OutfitListEventAction](
_ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
)
}
}
object OutfitListEvent extends Marshallable[OutfitListEvent] {
import shapeless.{::, HNil}
object PacketType extends Enumeration {
type Type = Value
val Unk0: PacketType.Value = Value(0)
val Unk1: PacketType.Value = Value(1)
val ListElementOutfit: PacketType.Value = Value(2)
val Unk3: PacketType.Value = Value(3)
val Unk4: PacketType.Value = Value(4)
val Unk5: PacketType.Value = Value(5)
val unk6: PacketType.Value = Value(6)
val unk7: PacketType.Value = Value(7)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
private def selectFromType(code: Int): Codec[OutfitListEventAction] = {
import OutfitListEventAction.Codecs._
import scala.annotation.switch
((code: @switch) match {
case 0 => unknownCodec(action = code)
case 1 => unknownCodec(action = code)
case 2 => ListElementOutfitCodec
case 3 => Unk3Codec
case 4 => unknownCodec(action = code)
case 5 => unknownCodec(action = code)
case 6 => unknownCodec(action = code)
case 7 => unknownCodec(action = code)
case _ => failureCodec(code)
}).asInstanceOf[Codec[OutfitListEventAction]]
}
implicit val codec: Codec[OutfitListEvent] = (
("packet_type" | PacketType.codec) >>:~ { packet_type =>
("action" | selectFromType(packet_type.id)).hlist
}
).xmap[OutfitListEvent](
{
case _ :: action :: HNil =>
OutfitListEvent(action)
},
{
case OutfitListEvent(action) =>
OutfitListEvent.PacketType(action.code) :: action :: HNil
}
)
}

View file

@ -0,0 +1,150 @@
// Copyright (c) 2025 PSForever
package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.{Attempt, Codec, Err}
import scodec.bits.BitVector
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitMemberEvent(
outfit_id: Long,
member_id: Long,
action: OutfitMemberEventAction
) extends PlanetSideGamePacket {
type Packet = OutfitMemberEvent
def opcode: Type = GamePacketOpcode.OutfitMemberEvent
def encode: Attempt[BitVector] = OutfitMemberEvent.encode(this)
}
abstract class OutfitMemberEventAction(val code: Int)
object OutfitMemberEventAction {
object PacketType extends Enumeration {
type Type = Value
val Unk0: PacketType.Value = Value(0)
val Padding: PacketType.Value = Value(1)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(1))
}
/*
action is unimplemented! if action == 0 unk2 will contain one additional uint32L
padding contains one uint4L of padding. may contain uint32L of unknown data depending on action
*/
final case class Unk0(
member_name: String,
rank: Int,
points: Long, // client divides this by 100
last_online: Long, // seconds ago from current time, 0 if online
action: PacketType.Type, // should always be 1, otherwise there will be actual data in padding. not implemented!
padding: Int // should always be 0, 4 bits of padding // only contains data if action is 0
) extends OutfitMemberEventAction(code = 0)
final case class Unk1(
) extends OutfitMemberEventAction(code = 1)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitMemberEventAction(badCode)
/**
* The `Codec`s used to transform the input stream into the context of a specific action
* and extract the field data from that stream.
*/
object Codecs {
private val everFailCondition = conditional(included = false, bool)
val Unk0Codec: Codec[Unk0] = (
("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only
("rank" | uint(3)) ::
("points" | uint32L) ::
("last_login" | uint32L) ::
("action" | OutfitMemberEventAction.PacketType.codec) ::
("padding" | uint4L)
).xmap[Unk0](
{
case member_name :: rank :: points :: last_login :: action :: padding :: HNil =>
Unk0(member_name, rank, points, last_login, action, padding)
},
{
case Unk0(member_name, rank, points, last_login, action, padding) =>
member_name :: rank :: points :: last_login :: action :: padding :: HNil
}
)
val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1())
/**
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
* @param action the action behavior code
* @return a transformation between the action code and the unknown bit data
*/
def unknownCodec(action: Int): Codec[Unknown] =
bits.xmap[Unknown](
data => Unknown(action, data),
{
case Unknown(_, data) => data
}
)
/**
* The action code was completely unanticipated!
* @param action the action behavior code
* @return nothing; always fail
*/
def failureCodec(action: Int): Codec[OutfitMemberEventAction] =
everFailCondition.exmap[OutfitMemberEventAction](
_ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
)
}
}
object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] {
object PacketType extends Enumeration {
type Type = Value
val Unk0: PacketType.Value = Value(0)
val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player
val Unk2: PacketType.Value = Value(2)
val Unk3: PacketType.Value = Value(3)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(2))
}
private def selectFromType(code: Int): Codec[OutfitMemberEventAction] = {
import OutfitMemberEventAction.Codecs._
import scala.annotation.switch
((code: @switch) match {
case 0 => Unk0Codec
case 1 => Unk1Codec
case 2 => unknownCodec(code)
case 3 => unknownCodec(code)
case _ => failureCodec(code)
}).asInstanceOf[Codec[OutfitMemberEventAction]]
}
implicit val codec: Codec[OutfitMemberEvent] = (
("packet_type" | PacketType.codec) >>:~ { packet_type =>
("outfit_id" | uint32L) ::
("member_id" | uint32L) ::
("action" | selectFromType(packet_type.id)).hlist
}
).xmap[OutfitMemberEvent](
{
case _ :: outfit_id :: member_id:: action :: HNil =>
OutfitMemberEvent(outfit_id, member_id, action)
},
{
case OutfitMemberEvent(outfit_id, member_id, action) =>
OutfitMemberEvent.PacketType(action.code) :: outfit_id :: member_id :: action :: HNil
}
)
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2025 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitMemberUpdate(
outfit_id: Long,
char_id: Long,
rank: Int, // 0-7
flag: Boolean,
) extends PlanetSideGamePacket {
type Packet = OutfitMemberUpdate
def opcode = GamePacketOpcode.OutfitMemberUpdate
def encode = OutfitMemberUpdate.encode(this)
}
object OutfitMemberUpdate extends Marshallable[OutfitMemberUpdate] {
implicit val codec: Codec[OutfitMemberUpdate] = (
("outfit_id" | uint32L) ::
("char_id" | uint32L) ::
("rank" | uint(3)) ::
("flag" | bool)
).xmap[OutfitMemberUpdate](
{
case outfit_id :: char_id :: rank :: flag :: HNil =>
OutfitMemberUpdate(outfit_id, char_id, rank, flag)
},
{
case OutfitMemberUpdate(outfit_id, char_id, rank, flag) =>
outfit_id :: char_id :: rank :: flag :: HNil
}
)
}

View file

@ -1,19 +1,16 @@
// Copyright (c) 2023 PSForever
// Copyright (c) 2023-2025 PSForever
package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideGUID
import scodec.{Attempt, Codec, Err}
import scodec.bits.BitVector
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitMembershipRequest(
request_type: OutfitMembershipRequest.RequestType.Type,
avatar_guid: PlanetSideGUID,
unk1: Int,
action: OutfitAction
outfit_id: Long,
action: OutfitMembershipRequestAction
) extends PlanetSideGamePacket {
type Packet = OutfitMembershipRequest
@ -22,20 +19,53 @@ final case class OutfitMembershipRequest(
def encode: Attempt[BitVector] = OutfitMembershipRequest.encode(this)
}
abstract class OutfitAction(val code: Int)
object OutfitAction {
abstract class OutfitMembershipRequestAction(val code: Int)
final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 0)
/*
Codecs 2,5,6,7 can either work off of the avatar_id (if GUI was used) or member_name (if chat command was used)
*/
object OutfitMembershipRequestAction {
final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 1)
final case class Create(
unk1: String,
outfit_name: String
) extends OutfitMembershipRequestAction(code = 0)
final case class AcceptOutfitInvite(unk2: String) extends OutfitAction(code = 3)
final case class Form(
unk1: String,
outfit_name: String
) extends OutfitMembershipRequestAction(code = 1)
final case class RejectOutfitInvite(unk2: String) extends OutfitAction(code = 4)
final case class Invite(
avatar_id: Long,
member_name: String,
) extends OutfitMembershipRequestAction(code = 2)
final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitAction(code = 5)
final case class AcceptInvite(
member_name: String
) extends OutfitMembershipRequestAction(code = 3)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitAction(badCode)
final case class RejectInvite(
member_name: String
) extends OutfitMembershipRequestAction(code = 4)
final case class CancelInvite(
avatar_id: Long,
member_name: String,
) extends OutfitMembershipRequestAction(code = 5)
final case class Kick(
avatar_id: Long,
member_name: String,
) extends OutfitMembershipRequestAction(code = 6)
final case class SetRank(
avatar_id: Long,
rank: Int,
member_name: String,
) extends OutfitMembershipRequestAction(code = 7)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipRequestAction(badCode)
/**
* The `Codec`s used to transform the input stream into the context of a specific action
@ -44,68 +74,121 @@ object OutfitAction {
object Codecs {
private val everFailCondition = conditional(included = false, bool)
val CreateOutfitCodec: Codec[CreateOutfit] =
(PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[CreateOutfit](
{
case unk2 :: unk3 :: unk4 :: outfit_name :: HNil =>
CreateOutfit(unk2, unk3, unk4, outfit_name)
},
{
case CreateOutfit(unk2, unk3, unk4, outfit_name) =>
unk2 :: unk3 :: unk4 :: outfit_name :: HNil
}
)
val CreateCodec: Codec[Create] = (
PacketHelpers.encodedWideStringAligned(5) ::
PacketHelpers.encodedWideString
).xmap[Create](
{
case u1 :: outfit_name :: HNil =>
Create(u1, outfit_name)
},
{
case Create(u1, outfit_name) =>
u1 :: outfit_name :: HNil
}
)
val FormOutfitCodec: Codec[FormOutfit] =
(PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[FormOutfit](
{
case unk2 :: unk3 :: unk4 :: outfit_name :: HNil =>
FormOutfit(unk2, unk3, unk4, outfit_name)
},
{
case FormOutfit(unk2, unk3, unk4, outfit_name) =>
unk2 :: unk3 :: unk4 :: outfit_name :: HNil
}
)
val FormCodec: Codec[Form] = (
PacketHelpers.encodedWideStringAligned(5) ::
PacketHelpers.encodedWideString
).xmap[Form](
{
case u1 :: outfit_name :: HNil =>
Form(u1, outfit_name)
},
{
case Form(u1, outfit_name) =>
u1 :: outfit_name :: HNil
}
)
val AcceptOutfitCodec: Codec[AcceptOutfitInvite] =
PacketHelpers.encodedWideString.xmap[AcceptOutfitInvite](
{
case unk2 =>
AcceptOutfitInvite(unk2)
},
{
case AcceptOutfitInvite(unk2) =>
unk2
}
)
val InviteCodec: Codec[Invite] = (
uint32L ::
PacketHelpers.encodedWideStringAligned(5)
).xmap[Invite](
{
case avatar_id :: member_name :: HNil =>
Invite(avatar_id, member_name)
},
{
case Invite(avatar_id, member_name) =>
avatar_id :: member_name :: HNil
}
)
val RejectOutfitCodec: Codec[RejectOutfitInvite] =
PacketHelpers.encodedWideString.xmap[RejectOutfitInvite](
{
case unk2 =>
RejectOutfitInvite(unk2)
},
{
case RejectOutfitInvite(unk2) =>
unk2
}
)
val AcceptInviteCodec: Codec[AcceptInvite] = (
PacketHelpers.encodedWideString
).xmap[AcceptInvite](
{
case member_name =>
AcceptInvite(member_name)
},
{
case AcceptInvite(member_name) =>
member_name
}
)
val RejectInviteCodec: Codec[RejectInvite] = (
PacketHelpers.encodedWideString
).xmap[RejectInvite](
{
case member_name =>
RejectInvite(member_name)
},
{
case RejectInvite(member_name) =>
member_name
}
)
val CancelInviteCodec: Codec[CancelInvite] = (
uint32L ::
PacketHelpers.encodedWideStringAligned(5)
).xmap[CancelInvite](
{
case avatar_id :: outfit_name :: HNil =>
CancelInvite(avatar_id, outfit_name)
},
{
case CancelInvite(avatar_id, outfit_name) =>
avatar_id :: outfit_name :: HNil
}
)
val KickCodec: Codec[Kick] = (
uint32L ::
PacketHelpers.encodedWideStringAligned(5)
).xmap[Kick](
{
case avatar_id :: member_name :: HNil =>
Kick(avatar_id, member_name)
},
{
case Kick(avatar_id, member_name) =>
avatar_id :: member_name :: HNil
}
)
val SetRankCodec: Codec[SetRank] = (
uint32L ::
uintL(3) ::
PacketHelpers.encodedWideStringAligned(2)
).xmap[SetRank](
{
case avatar_id :: rank :: member_name :: HNil =>
SetRank(avatar_id, rank, member_name)
},
{
case SetRank(avatar_id, rank, member_name) =>
avatar_id :: rank :: member_name :: HNil
}
)
val CancelOutfitCodec: Codec[CancelOutfitInvite] =
(uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[CancelOutfitInvite](
{
case unk5 :: unk6 :: outfit_name :: HNil =>
CancelOutfitInvite(unk5, unk6, outfit_name)
},
{
case CancelOutfitInvite(unk5, unk6, outfit_name) =>
unk5 :: unk6 :: outfit_name :: HNil
}
)
/**
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
*
* @param action the action behavior code
* @return a transformation between the action code and the unknown bit data
*/
@ -119,11 +202,12 @@ object OutfitAction {
/**
* The action code was completely unanticipated!
*
* @param action the action behavior code
* @return nothing; always fail
*/
def failureCodec(action: Int): Codec[OutfitAction] =
everFailCondition.exmap[OutfitAction](
def failureCodec(action: Int): Codec[OutfitMembershipRequestAction] =
everFailCondition.exmap[OutfitMembershipRequestAction](
_ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
)
@ -132,53 +216,52 @@ object OutfitAction {
object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] {
object RequestType extends Enumeration {
object PacketType extends Enumeration {
type Type = Value
val Create: RequestType.Value = Value(0)
val Form: RequestType.Value = Value(1)
val Unk2: RequestType.Value = Value(2)
val Accept: RequestType.Value = Value(3)
val Reject: RequestType.Value = Value(4)
val Cancel: RequestType.Value = Value(5)
val Unk6: RequestType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
val Unk7: RequestType.Value = Value(7)
val Create: PacketType.Value = Value(0)
val Form: PacketType.Value = Value(1)
val Invite: PacketType.Value = Value(2)
val Accept: PacketType.Value = Value(3)
val Reject: PacketType.Value = Value(4)
val Cancel: PacketType.Value = Value(5)
val Kick: PacketType.Value = Value(6)
val SetRank: PacketType.Value = Value(7)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
private def selectFromType(code: Int): Codec[OutfitAction] = {
import OutfitAction.Codecs._
private def selectFromType(code: Int): Codec[OutfitMembershipRequestAction] = {
import OutfitMembershipRequestAction.Codecs._
import scala.annotation.switch
((code: @switch) match {
case 0 => CreateOutfitCodec
case 1 => FormOutfitCodec // so far same as Create
case 2 => unknownCodec(action = code)
case 3 => AcceptOutfitCodec
case 4 => RejectOutfitCodec // so far same as Accept
case 5 => CancelOutfitCodec
case 6 => unknownCodec(action = code)
case 7 => unknownCodec(action = code)
// 3 bit limit
case 0 => CreateCodec
case 1 => FormCodec // so far same as Create
case 2 => InviteCodec
case 3 => AcceptInviteCodec
case 4 => RejectInviteCodec
case 5 => CancelInviteCodec
case 6 => KickCodec
case 7 => SetRankCodec
case _ => failureCodec(code)
}).asInstanceOf[Codec[OutfitAction]]
}).asInstanceOf[Codec[OutfitMembershipRequestAction]]
}
implicit val codec: Codec[OutfitMembershipRequest] = (
("request_type" | RequestType.codec) >>:~ { request_type =>
("avatar_guid" | PlanetSideGUID.codec) ::
("unk1" | uint16L) ::
("action" | selectFromType(request_type.id))
("packet_type" | PacketType.codec) >>:~ { packet_type =>
("outfit_id" | uint32L) ::
("action" | selectFromType(packet_type.id))
}
).xmap[OutfitMembershipRequest](
{
case request_type :: avatar_guid :: u1 :: action :: HNil =>
OutfitMembershipRequest(request_type, avatar_guid, u1, action)
case _ :: outfit_id :: action :: HNil =>
OutfitMembershipRequest(outfit_id, action)
},
{
case OutfitMembershipRequest(request_type, avatar_guid, u1, action) =>
request_type :: avatar_guid :: u1 :: action :: HNil
case OutfitMembershipRequest(outfit_id, action) =>
OutfitMembershipRequest.PacketType(action.code) :: outfit_id :: action :: HNil
}
)
}

View file

@ -0,0 +1,64 @@
// Copyright (c) 2025 PSForever
package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.{Attempt, Codec}
import scodec.bits.BitVector
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitMembershipResponse(
packet_type: OutfitMembershipResponse.PacketType.Type,
unk0: Int,
unk1: Int,
outfit_id: Long,
target_id: Long,
str1: String,
str2: String,
flag: Boolean
) extends PlanetSideGamePacket {
type Packet = OutfitMembershipResponse
def opcode: Type = GamePacketOpcode.OutfitMembershipResponse
def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this)
}
object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] {
object PacketType extends Enumeration {
type Type = Value
val CreateResponse: PacketType.Value = Value(0)
val Invite: PacketType.Value = Value(1) // response to OutfitMembershipRequest Unk2 for that player
val InviteAccepted: PacketType.Value = Value(2)
val InviteRejected: PacketType.Value = Value(3)
val Unk4: PacketType.Value = Value(4)
val Kick: PacketType.Value = Value(5)
val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
val Unk7: PacketType.Value = Value(7)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
implicit val codec: Codec[OutfitMembershipResponse] = (
("packet_type" | PacketType.codec) ::
("unk0" | uintL(5)) ::
("unk1" | uintL(3)) ::
("outfit_id" | uint32L) ::
("target_id" | uint32L) ::
("str1" | PacketHelpers.encodedWideStringAligned(5)) ::
("str2" | PacketHelpers.encodedWideString) ::
("flag" | bool)
).xmap[OutfitMembershipResponse](
{
case packet_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil =>
OutfitMembershipResponse(packet_type, u0, u1, outfit_id, target_id, str1, str2, flag)
},
{
case OutfitMembershipResponse(packet_type, u0, u1, outfit_id, target_id, str1, str2, flag) =>
packet_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil
}
)
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2023 PSForever
// Copyright (c) 2023-2025 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
@ -7,77 +7,77 @@ import scodec.bits.ByteVector
import scodec.codecs._
import shapeless.{::, HNil}
/**
* na
*/
abstract class OutfitRequestForm(val code: Int)
object OutfitRequestForm {
/**
* na
* @param str na
*/
final case class Unk0(str: String) extends OutfitRequestForm(code = 0)
/**
* na
* @param list na
*/
final case class Unk1(list: List[Option[String]]) extends OutfitRequestForm(code = 1)
/**
* na
* @param unk na
*/
final case class Unk2(unk: Int) extends OutfitRequestForm(code = 2)
/**
* na
* @param unk na
*/
final case class Unk3(unk: Boolean) extends OutfitRequestForm(code = 3)
/**
* na
* @param unk na
*/
final case class Unk4(unk: Boolean) extends OutfitRequestForm(code = 4)
/**
* na
* @param unk na
*/
final case class Fail(unk: ByteVector) extends OutfitRequestForm(code = -1)
final case class OutfitRequest(
outfit_id: Long,
action: OutfitRequestAction
) extends PlanetSideGamePacket {
type Packet = OutfitRequest
def opcode = GamePacketOpcode.OutfitRequest
def encode = OutfitRequest.encode(this)
}
/**
* na
* @param id na
* @param info na
*/
final case class OutfitRequest(id: Long, info: OutfitRequestForm)
extends PlanetSideGamePacket {
type Packet = OrbitalStrikeWaypointMessage
def opcode = GamePacketOpcode.OutfitRequest
def encode = OutfitRequest.encode(this)
abstract class OutfitRequestAction(val code: Int)
object OutfitRequestAction {
/**
* na
* @param str na
*/
final case class Motd(str: String) extends OutfitRequestAction(code = 0)
/**
* na
* @param list na
*/
final case class Ranks(list: List[Option[String]]) extends OutfitRequestAction(code = 1)
/**
* na
* @param unk na
*/
final case class Unk2(unk: Int) extends OutfitRequestAction(code = 2)
/**
* na
* @param unk na
*/
final case class Unk3(menuOpen: Boolean) extends OutfitRequestAction(code = 3)
/**
* na
* @param unk na
*/
final case class Unk4(menuOpen: Boolean) extends OutfitRequestAction(code = 4)
/**
* na
* @param unk na
*/
final case class Fail(unk: ByteVector) extends OutfitRequestAction(code = -1)
}
object OutfitRequest extends Marshallable[OutfitRequest] {
/**
* na
*/
private val unk0Codec: Codec[OutfitRequestForm] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist
.xmap[OutfitRequestForm] (
private val MotdCodec: Codec[OutfitRequestAction] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist
.xmap[OutfitRequestAction] (
{
case value :: HNil => OutfitRequestForm.Unk0(value)
case value :: HNil => OutfitRequestAction.Motd(value)
},
{
case OutfitRequestForm.Unk0(value) => value :: HNil
case OutfitRequestAction.Motd(value) => value :: HNil
}
)
/**
* na
*/
private val unk1Codec: Codec[OutfitRequestForm] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestForm] (
list => OutfitRequestForm.Unk1(list),
private val RankCodec: Codec[OutfitRequestAction] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestAction] (
list => OutfitRequestAction.Ranks(list),
{
case OutfitRequestForm.Unk1(list) => list
case OutfitRequestAction.Ranks(list) => list
}
)
@ -104,54 +104,66 @@ object OutfitRequest extends Marshallable[OutfitRequest] {
/**
* na
*/
private val unk2Codec: Codec[OutfitRequestForm] = uint8.hlist.xmap[OutfitRequestForm] (
private val unk2Codec: Codec[OutfitRequestAction] = uint8.hlist.xmap[OutfitRequestAction] (
{
case value :: HNil => OutfitRequestForm.Unk2(value)
case value :: HNil => OutfitRequestAction.Unk2(value)
},
{
case OutfitRequestForm.Unk2(value) => value :: HNil
case OutfitRequestAction.Unk2(value) => value :: HNil
}
)
/**
* na
*/
private val unk3Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] (
private val unk3Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] (
{
case value :: HNil => OutfitRequestForm.Unk3(value)
case value :: HNil => OutfitRequestAction.Unk3(value)
},
{
case OutfitRequestForm.Unk3(value) => value :: HNil
case OutfitRequestAction.Unk3(value) => value :: HNil
}
)
/**
* na
*/
private val unk4Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] (
private val unk4Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] (
{
case value :: HNil => OutfitRequestForm.Unk4(value)
case value :: HNil => OutfitRequestAction.Unk4(value)
},
{
case OutfitRequestForm.Unk4(value) => value :: HNil
case OutfitRequestAction.Unk4(value) => value :: HNil
}
)
/**
* na
*/
private def failCodec(code: Int): Codec[OutfitRequestForm] = conditional(included = false, bool).exmap[OutfitRequestForm](
_ => Attempt.Failure(Err(s"can not decode $code-type info - what is this thing?")),
_ => Attempt.Failure(Err(s"can not encode $code-type info - no such thing"))
private def failCodec(action: Int): Codec[OutfitRequestAction] = conditional(included = false, bool).exmap[OutfitRequestAction](
_ => Attempt.Failure(Err(s"can not decode $action-type info - what is this thing?")),
_ => Attempt.Failure(Err(s"can not encode $action-type info - no such thing"))
)
object PacketType extends Enumeration {
type Type = Value
val Motd: PacketType.Value = Value(0)
val Rank: PacketType.Value = Value(1)
val Unk2: PacketType.Value = Value(2)
val Detail: PacketType.Value = Value(3)
val List: PacketType.Value = Value(4) // sent by client if menu is either open (true) or closed (false)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
/**
* na
*/
private def infoCodec(code: Int): Codec[OutfitRequestForm] = {
private def selectFromType(code: Int): Codec[OutfitRequestAction] = {
code match {
case 0 => unk0Codec
case 1 => unk1Codec
case 0 => MotdCodec
case 1 => RankCodec
case 2 => unk2Codec
case 3 => unk3Codec
case 4 => unk4Codec
@ -160,18 +172,18 @@ object OutfitRequest extends Marshallable[OutfitRequest] {
}
implicit val codec: Codec[OutfitRequest] = (
uint(bits = 3) >>:~ { code =>
("packet_type" | PacketType.codec) >>:~ { packet_type =>
("id" | uint32L) ::
("info" | infoCodec(code))
("action" | selectFromType(packet_type.id)).hlist
}
).xmap[OutfitRequest](
).xmap[OutfitRequest](
{
case _:: id:: info :: HNil =>
OutfitRequest(id, info)
case _ :: id:: action :: HNil =>
OutfitRequest(id, action)
},
{
case OutfitRequest(id, info) =>
info.code :: id :: info :: HNil
case OutfitRequest(id, action) =>
OutfitRequest.PacketType(action.code) :: id :: action :: HNil
}
)
}

View file

@ -82,9 +82,7 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
Some(outfit_id)
) =>
Attempt.Successful(
MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(
outfit_id
) :: HNil
MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(outfit_id) :: HNil
)
case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) =>
Attempt.Successful(

View file

@ -198,6 +198,14 @@ class AvatarService(zone: Zone) extends Actor {
AvatarResponse.PlanetsideAttributeSelf(attribute_type, attribute_value)
)
)
case AvatarAction.PlanetsideStringAttribute(guid, attribute_type, attribute_value) =>
AvatarEvents.publish(
AvatarServiceResponse(
s"/$forChannel/Avatar",
guid,
AvatarResponse.PlanetsideStringAttribute(attribute_type, attribute_value)
)
)
case AvatarAction.PlayerState(
guid,
pos,

View file

@ -78,6 +78,8 @@ object AvatarAction {
extends Action
final case class PlanetsideAttributeSelf(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long)
extends Action
final case class PlanetsideStringAttribute(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: String)
extends Action
final case class PlayerState(
player_guid: PlanetSideGUID,
pos: Vector3,

View file

@ -47,7 +47,7 @@ object AvatarResponse {
final case class EquipmentInHand(pkt: ObjectCreateMessage) extends Response
final case class GenericObjectAction(object_guid: PlanetSideGUID, action_code: Int) extends Response
final case class HitHint(source_guid: PlanetSideGUID) extends Response
final case class Killed(cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Response
final case class Killed(cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Response
final case class LoadPlayer(pkt: ObjectCreateMessage) extends Response
final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response
final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) extends Response
@ -56,6 +56,7 @@ object AvatarResponse {
final case class PlanetsideAttribute(attribute_type: Int, attribute_value: Long) extends Response
final case class PlanetsideAttributeToAll(attribute_type: Int, attribute_value: Long) extends Response
final case class PlanetsideAttributeSelf(attribute_type: Int, attribute_value: Long) extends Response
final case class PlanetsideStringAttribute(attribute_type: Int, attribute_value: String) extends Response
final case class PlayerState(
pos: Vector3,
vel: Option[Vector3],

View file

@ -12,3 +12,5 @@ final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel
case object SpectatorChannel extends ChatChannel
case object CustomerServiceChannel extends ChatChannel
final case class OutfitChannel(id: Long) extends ChatChannel

View file

@ -57,6 +57,7 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
(channel, message.messageType) match {
case (SquadChannel(_), CMT_SQUAD) => ()
case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
case (OutfitChannel(_), CMT_OUTFIT) => ()
case (DefaultChannel, messageType) if messageType != CMT_SQUAD => ()
case (SpectatorChannel, messageType) if messageType != CMT_SQUAD => ()
case _ =>
@ -158,6 +159,9 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
case CMT_SQUAD =>
subs.foreach(_.actor ! MessageResponse(session, message, channel))
case CMT_OUTFIT =>
subs.foreach(_.actor ! MessageResponse(session, message, channel))
case CMT_NOTE =>
subs
.filter(_.sessionSource.session.player.Name == message.recipient)