mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-19 18:14:44 +00:00
Merge branch 'even-more-outfit-packets-2' into outfit
This commit is contained in:
commit
446dee8235
107
server/src/main/resources/db/migration/V015__OutfitStructure.sql
Normal file
107
server/src/main/resources/db/migration/V015__OutfitStructure.sql
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
-- Tables for Outfits
|
||||
--
|
||||
-- Includes Outfit, OutfitMember and OutfitPoints
|
||||
|
||||
/*
|
||||
|
||||
This migration allows for the storage of all outfit relevant data.
|
||||
|
||||
Outfit
|
||||
- each outfit has one entry in the outfit table
|
||||
- each name is unique and ranks are inlined (static 1:n, join unnecessary)
|
||||
- faction is limited to 0,1,2,3
|
||||
- decal is limited to 0 through 26 (inclusive)
|
||||
|
||||
OutfitMember
|
||||
- each avatar can at most be a member in one outfit
|
||||
- each outfit can only have one rank 7 (leader) member
|
||||
- rank is limited to 0 through 7 (inclusive)
|
||||
- there is a "quick access" index on outfit and avatar for rank 7 (leader)
|
||||
|
||||
OutfitPoint
|
||||
- each (outfit, avatar) combination can only exist once
|
||||
- a (outfit, NULL) combination can not be limited yet (not a big deal)
|
||||
- deleting a avatar will have his points remain as (outfit, NULL)
|
||||
- leaving an outfit will have the points remain as (outfit, NULL)
|
||||
|
||||
*/
|
||||
|
||||
-- OUTFIT
|
||||
|
||||
CREATE TABLE outfit (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"created" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT FALSE, -- allow for recovery of accidentially deleted outfits
|
||||
"faction" SMALLINT NOT NULL,
|
||||
"owner_id" INTEGER NOT NULL,
|
||||
"decal" SMALLINT NOT NULL DEFAULT 0,
|
||||
"name" VARCHAR(32) NOT NULL,
|
||||
"motd" VARCHAR(255) NULL,
|
||||
"rank0" VARCHAR(32) NULL, -- Non-Officer Rank 1
|
||||
"rank1" VARCHAR(32) NULL,
|
||||
"rank2" VARCHAR(32) NULL,
|
||||
"rank3" VARCHAR(32) NULL, -- Non-Officer Rank 4
|
||||
"rank4" VARCHAR(32) NULL, -- Fourth In Command
|
||||
"rank5" VARCHAR(32) NULL,
|
||||
"rank6" VARCHAR(32) NULL, -- Second In Command
|
||||
"rank7" VARCHAR(32) NULL, -- Outfit Leader
|
||||
|
||||
CONSTRAINT "outfit_faction_check" CHECK("faction" BETWEEN 0 AND 3), -- allowed faction IDs
|
||||
CONSTRAINT "outfit_decal_check" CHECK("decal" BETWEEN 0 AND 26), -- allowed decal IDs
|
||||
|
||||
CONSTRAINT "outfit_owner_id_avatar_id_fkey" FOREIGN KEY ("owner_id") REFERENCES avatar ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "outfit_created_brin_idx" ON "outfit" USING BRIN ("created"); -- super small, index for physically sequential data
|
||||
CREATE INDEX "outfit_faction_deleted_idx" ON "outfit" ("faction", "deleted"); -- optimize index for search: SELECT * FROM "outfit" WHERE "faction" = ? AND "deleted" = false;
|
||||
|
||||
-- OUTFITMEMBER
|
||||
|
||||
CREATE TABLE outfitmember (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"created" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"outfit_id" INTEGER NOT NULL,
|
||||
"avatar_id" INTEGER NOT NULL,
|
||||
"rank" SMALLINT NOT NULL DEFAULT 0, -- lowest rank
|
||||
|
||||
CONSTRAINT "outfitmember_rank_check" CHECK("rank" BETWEEN 0 AND 7), -- allowed ranks
|
||||
|
||||
CONSTRAINT "outfitmember_outfit_id_outfit_id_fkey" FOREIGN KEY ("outfit_id") REFERENCES outfit ("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "outfitmember_avatar_id_avatar_id_fkey" FOREIGN KEY ("avatar_id") REFERENCES avatar ("id") ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX "outfitmember_outfit_id_idx" ON "outfitmember" ("outfit_id"); -- FK index
|
||||
CREATE UNIQUE INDEX "outfitmember_avatar_id_unique" ON "outfitmember" ("avatar_id"); -- FK index, enforce one outfit per avatar
|
||||
CREATE UNIQUE INDEX "outfitmember_outfit_id_rank_partial_leader_unique" ON "outfitmember" ("outfit_id", "rank") WHERE "rank" = 7; -- quick access to outfit leader and ony one leader per outfit
|
||||
|
||||
-- OUTFITPOINT
|
||||
|
||||
CREATE TABLE outfitpoint (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"outfit_id" INTEGER NOT NULL,
|
||||
"avatar_id" INTEGER NULL,
|
||||
"points" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "outfitpoint_points_check" CHECK ("points" >= 0),
|
||||
|
||||
-- enforce unique combinations (left side index)
|
||||
CONSTRAINT "outfitpoint_outfit_avatar_unique_idx" UNIQUE ("outfit_id", "avatar_id"), -- UNIQUE NULLS NOT DISTINCT
|
||||
|
||||
CONSTRAINT "outfitpoint_outfit_fkey" FOREIGN KEY ("outfit_id") REFERENCES outfit ("id") ON DELETE CASCADE, -- delete points of outfit when outfit is deleted
|
||||
CONSTRAINT "outfitpoint_avatar_fkey" FOREIGN KEY ("avatar_id") REFERENCES avatar ("id") ON DELETE SET NULL -- keep points for outfit when player is deleted
|
||||
);
|
||||
|
||||
-- add right side index (avatar_id)
|
||||
CREATE INDEX "outfitpoint_avatar_idx" ON "outfitpoint" ("avatar_id");
|
||||
|
||||
-- MATERIALIZED VIEW for OUTFITPOINT
|
||||
|
||||
CREATE MATERIALIZED VIEW outfitpoint_mv AS
|
||||
SELECT
|
||||
"outfit_id",
|
||||
SUM("points") as "points"
|
||||
FROM
|
||||
"outfitpoint"
|
||||
GROUP BY "outfit_id";
|
||||
|
||||
CREATE UNIQUE INDEX "outfitpoint_mv_outfit_id_unique" ON "outfitpoint_mv" ("outfit_id");
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 = { /* */ }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 */ }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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 ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
263
src/main/scala/net/psforever/packet/game/OutfitEvent.scala
Normal file
263
src/main/scala/net/psforever/packet/game/OutfitEvent.scala
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
158
src/main/scala/net/psforever/packet/game/OutfitListEvent.scala
Normal file
158
src/main/scala/net/psforever/packet/game/OutfitListEvent.scala
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
150
src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala
Normal file
150
src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
241
src/test/scala/game/OutfitEventTest.scala
Normal file
241
src/test/scala/game/OutfitEventTest.scala
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.OutfitEvent
|
||||
import net.psforever.packet.game.OutfitEventAction._
|
||||
import org.specs2.mutable._
|
||||
import scodec.bits._
|
||||
|
||||
class OutfitEventTest extends Specification {
|
||||
|
||||
val unk0_ABC: ByteVector = ByteVector.fromValidHex(
|
||||
"8f 1 a8c2 0001" + // packet head
|
||||
"2a 0 42006c00610063006b002000410072006d006f0072006500640020005200650061007000650072007300" + // Black Armored Reapers
|
||||
"1d9c4d0d" +
|
||||
"1d9c4d0d" +
|
||||
"ab00 0000" +
|
||||
"88 44006f00670020004d00650061007400" + // Dog Meat
|
||||
"87 5200750073007300690061006e00" + // Russian
|
||||
"80" + //
|
||||
"80" + //
|
||||
"8d 5300710075006100640020004c00650061006400650072007300" + // Squad Leaders
|
||||
"91 41006300740069006e006700200043006f006d006d0061006e006400650072007300" + // Acting Commanders
|
||||
"87 5200650061007000650072007300" + // Reapers
|
||||
"80" + //
|
||||
"00" +
|
||||
"9c 5c0023003000300030003000660066004d0075006d0062006c00650020005c00230030003000330033006600660049006e0066006f0020005c0023003000300036003600660066006900730020005c0023003000300039003900660066007400680065006d006f006f00730065002e00740079007000650066007200610067002e0063006f006d0020005c00230030003000630063006600660070006f007200740020005c002300300030006600660066006600390033003500300020005c0023003000300063006300660066006a006f0069006e0020005c0023003000300039003900660066006900740020005c0023003000300036003600660066006f00720020005c0023003000300033003300660066006200650020005c0023003000300030003000660066006b00690063006b00650064002e00" +
|
||||
"0f80" +
|
||||
"0000 00737296 24000000 00000000 00000000 0000")
|
||||
val unk1_ABC: ByteVector = hex"8f 2 302a 10 00 0"
|
||||
val unk2_ABC: ByteVector = ByteVector.fromValidHex(
|
||||
"8f 4 0201feff" +
|
||||
"2e 0 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500" + // PlanetSide_Forever_Vanu
|
||||
"00000000" +
|
||||
"00000000" +
|
||||
"0100 0000" +
|
||||
"80" +
|
||||
"80" +
|
||||
"80" +
|
||||
"80" +
|
||||
"80" +
|
||||
"80" +
|
||||
"80" +
|
||||
"80" +
|
||||
"80" +
|
||||
"0070" +
|
||||
"4982 00000000 00000000 00000000 00000000 0000")
|
||||
val unk3_ABC: ByteVector = hex"8f 6 0201 fe fe 0"
|
||||
val unk4_ABC: ByteVector = hex"8f 8 0201 fefe a02a 1000 0"
|
||||
val unk5_ABC: ByteVector = hex"8f a 0201 fefe 0400 0000 0"
|
||||
|
||||
"decode Unk0 ABC" in {
|
||||
PacketCoding.decodePacket(unk0_ABC).require match {
|
||||
case OutfitEvent(outfit_guid, action) =>
|
||||
outfit_guid mustEqual 25044
|
||||
action mustEqual Unk0(
|
||||
OutfitInfo(
|
||||
outfit_name = "Black Armored Reapers",
|
||||
outfit_points1 = 223190045,
|
||||
outfit_points2 = 223190045,
|
||||
member_count = 171,
|
||||
OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""),
|
||||
"\\#0000ffMumble \\#0033ffInfo \\#0066ffis \\#0099ffthemoose.typefrag.com \\#00ccffport \\#00ffff9350 \\#00ccffjoin \\#0099ffit \\#0066ffor \\#0033ffbe \\#0000ffkicked.",
|
||||
15,
|
||||
unk11 = true,
|
||||
unk12 = 0,
|
||||
created_timestamp = 1210901990,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Unk0 ABC" in {
|
||||
val msg = OutfitEvent(
|
||||
25044,
|
||||
Unk0(
|
||||
OutfitInfo(
|
||||
outfit_name = "Black Armored Reapers",
|
||||
outfit_points1 = 223190045,
|
||||
outfit_points2 = 223190045,
|
||||
member_count = 171,
|
||||
OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""),
|
||||
"\\#0000ffMumble \\#0033ffInfo \\#0066ffis \\#0099ffthemoose.typefrag.com \\#00ccffport \\#00ffff9350 \\#00ccffjoin \\#0099ffit \\#0066ffor \\#0033ffbe \\#0000ffkicked.",
|
||||
15,
|
||||
unk11 = true,
|
||||
unk12 = 0,
|
||||
created_timestamp = 1210901990,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk0_ABC
|
||||
}
|
||||
|
||||
"decode Unk1 ABC" in {
|
||||
PacketCoding.decodePacket(unk1_ABC).require match {
|
||||
case OutfitEvent(outfit_guid, action) =>
|
||||
outfit_guid mustEqual 529688L
|
||||
action mustEqual Unk1()
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Unk1 ABC" in {
|
||||
val msg = OutfitEvent(
|
||||
529688L,
|
||||
Unk1()
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk1_ABC
|
||||
}
|
||||
|
||||
"decode Unk2 ABC" in {
|
||||
PacketCoding.decodePacket(unk2_ABC).require match {
|
||||
case OutfitEvent(outfit_guid, action) =>
|
||||
outfit_guid mustEqual 2147418113L
|
||||
action mustEqual Unk2(OutfitInfo(
|
||||
outfit_name = "PlanetSide_Forever_Vanu",
|
||||
outfit_points1 = 0,
|
||||
outfit_points2 = 0,
|
||||
member_count = 1,
|
||||
OutfitRankNames("","","","","","","",""),
|
||||
"",
|
||||
0,
|
||||
unk11 = false,
|
||||
unk12 = 300000,
|
||||
created_timestamp = 0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
))
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Unk2 ABC" in {
|
||||
val msg = OutfitEvent(
|
||||
2147418113L,
|
||||
Unk2(
|
||||
OutfitInfo(
|
||||
outfit_name = "PlanetSide_Forever_Vanu",
|
||||
outfit_points1 = 0,
|
||||
outfit_points2 = 0,
|
||||
member_count = 1,
|
||||
OutfitRankNames("","","","","","","",""),
|
||||
"",
|
||||
0,
|
||||
unk11 = false,
|
||||
unk12 = 300000,
|
||||
created_timestamp = 0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk2_ABC
|
||||
}
|
||||
|
||||
"decode Unk3 ABC" in {
|
||||
PacketCoding.decodePacket(unk3_ABC).require match {
|
||||
case OutfitEvent(outfit_guid, action) =>
|
||||
outfit_guid mustEqual 2147418113L
|
||||
action mustEqual Unk3()
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Unk3 ABC" in {
|
||||
val msg = OutfitEvent(
|
||||
2147418113L,
|
||||
Unk3()
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk3_ABC
|
||||
}
|
||||
|
||||
"decode Unk4 ABC" in {
|
||||
PacketCoding.decodePacket(unk4_ABC).require match {
|
||||
case OutfitEvent(outfit_guid, action) =>
|
||||
outfit_guid mustEqual 2147418113L
|
||||
action mustEqual UpdateOutfitId(
|
||||
new_outfit_id = 529744L,
|
||||
)
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Unk4 ABC" in {
|
||||
val msg = OutfitEvent(
|
||||
2147418113L,
|
||||
UpdateOutfitId(
|
||||
new_outfit_id = 529744L,
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk4_ABC
|
||||
}
|
||||
|
||||
"decode Unk5 ABC" in {
|
||||
PacketCoding.decodePacket(unk5_ABC).require match {
|
||||
case OutfitEvent(outfit_guid, action) =>
|
||||
outfit_guid mustEqual 2147418113L
|
||||
action mustEqual Unk5(
|
||||
unk1 = 2,
|
||||
)
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Unk5 ABC" in {
|
||||
val msg = OutfitEvent(
|
||||
2147418113L,
|
||||
Unk5(
|
||||
unk1 = 2,
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk5_ABC
|
||||
}
|
||||
}
|
||||
50
src/test/scala/game/OutfitListEventTest.scala
Normal file
50
src/test/scala/game/OutfitListEventTest.scala
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.OutfitListEvent
|
||||
import net.psforever.packet.game.OutfitListEventAction.ListElementOutfit
|
||||
import org.specs2.mutable._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
class OutfitListEventTest extends Specification {
|
||||
|
||||
val unk2_0_ABC: ByteVector = ByteVector.fromValidHex("98 5 e83a0000 000e1800 0800000 11404e0069006700680074004c006f00720064007300 854e005900430061007400")
|
||||
val unk2_0_DEF: ByteVector = ByteVector.fromValidHex("98 4 ec281001 51a62800 3400000 11a0490052004f004e004600490053005400200043006c0061006e00 8654006f006c006a00")
|
||||
val unk2_1_ABC: ByteVector = ByteVector.fromValidHex("98 4 723c0000 2aa81e00 2200000 11006900470061006d00650073002d004500 906900670061006d006500730043005400460057006800610063006b002d004500")
|
||||
val unk2_2_ABC: ByteVector = ByteVector.fromValidHex("98 4 9a3c0001 16da4e00 0400000 11a042006c006f006f00640020006f0066002000560061006e007500 864b00610072006e002d004500")
|
||||
val unk2_3_ABC: ByteVector = ByteVector.fromValidHex("98 4 9c3c0000 df587c00 1400000 11a054006800650020004e00650076006500720068006f006f006400 8e6f00460058006f00530074006f006e0065004d0061006e002d004700")
|
||||
val unk2_4_ABC: ByteVector = ByteVector.fromValidHex("98 4 c03c0000 24060400 0600000 1220540068006500200042006c00610063006b0020004b006e0069006700680074007300 874400720061007a00760065006e00")
|
||||
val unk2_5_ABC: ByteVector = ByteVector.fromValidHex("98 5 383c0001 4b709a00 0c00000 10a03e005400760053003c00 89430061007000650062006f00610074007300")
|
||||
val unk2_6_ABC: ByteVector = ByteVector.fromValidHex("98 5 b03c0000 35d67000 0400000 11404c006f0073007400200043006100750073006500 895a00650072006f004b00650077006c006c00")
|
||||
val unk2_7_ABC: ByteVector = ByteVector.fromValidHex("98 4 043e0001 9fb82616 1400000 11e0540068006500200042006c00610063006b00200054006f00770065007200 874b00720075007000680065007800")
|
||||
|
||||
"decode unk0_ABC" in {
|
||||
PacketCoding.decodePacket(unk2_0_ABC).require match {
|
||||
case OutfitListEvent(ListElementOutfit(unk1, points, members, outfit_name, outfit_leader)) =>
|
||||
unk1 mustEqual 7668
|
||||
points mustEqual 788224
|
||||
members mustEqual 4
|
||||
outfit_name mustEqual "NightLords"
|
||||
outfit_leader mustEqual "NYCat"
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode unk0_ABC" in {
|
||||
val msg = OutfitListEvent(
|
||||
ListElementOutfit(
|
||||
7668,
|
||||
788224,
|
||||
4,
|
||||
"NightLords",
|
||||
"NYCat"
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk2_0_ABC
|
||||
}
|
||||
|
||||
}
|
||||
112
src/test/scala/game/OutfitMemberEventTest.scala
Normal file
112
src/test/scala/game/OutfitMemberEventTest.scala
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.{OutfitMemberEvent, OutfitMemberEventAction}
|
||||
import net.psforever.packet.game.OutfitMemberEventAction._
|
||||
import org.specs2.mutable._
|
||||
import scodec.bits._
|
||||
|
||||
class OutfitMemberEventTest extends Specification {
|
||||
|
||||
//val unk0_ABC: ByteVector = hex"90 3518 4000 1a4e 4100 2 180 450078007000650072007400 8483 07e0 119d bfe0 70" // 0x90048640001030c28022404c0061007a00650072003100390038003200f43a45e00b4c604010
|
||||
val Lazer = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 f43a 45e0 0b4c 6040 10"
|
||||
val Lazer2 = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 e6dc 25a0 153e 6040 10"
|
||||
val OpolE = hex"90 0 4864 0003 aad6 280a 14 0 4f0070006f006c004500 c9a1 80e0 0d03 2040 10"
|
||||
val Billy = hex"90 0 4864 0003 a41a 280a 20 0 620069006c006c007900320035003600 935f 6000 186a b040 50"
|
||||
val Virus = hex"90 0 4864 0002 1b64 4c02 28 0 5600690072007500730047006900760065007200 2f89 0080 0000 0000 10"
|
||||
val PvtPa = hex"90 0 4864 0000 1e69 e80a 2c 0 500076007400500061006e00630061006b0065007300 705e a080 0a85 e060 10"
|
||||
val Night = hex"90 0 4864 0002 4cf0 3802 28 0 4e006900670068007400770069006e0067003100 b8fb 9a40 0da6 ec80 50"
|
||||
|
||||
val unk1 = hex"90 5 40542002 3f61e808 0"
|
||||
|
||||
"decode Lazer padding" in {
|
||||
PacketCoding.decodePacket(Lazer).require match {
|
||||
case OutfitMemberEvent(outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, padding)) =>
|
||||
outfit_id mustEqual 6418
|
||||
member_id mustEqual 705344
|
||||
member_name mustEqual "Lazer1982"
|
||||
rank mustEqual 7
|
||||
points mustEqual 3134113
|
||||
last_login mustEqual 156506
|
||||
action mustEqual OutfitMemberEventAction.PacketType.Padding
|
||||
padding mustEqual 0
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Lazer padding" in {
|
||||
val msg = OutfitMemberEvent(
|
||||
outfit_id = 6418,
|
||||
member_id = 705344,
|
||||
Unk0(
|
||||
member_name = "Lazer1982",
|
||||
rank = 7,
|
||||
points = 3134113,
|
||||
last_online = 156506,
|
||||
action = OutfitMemberEventAction.PacketType.Padding,
|
||||
padding = 0
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual Lazer
|
||||
}
|
||||
|
||||
"decode OpolE padding" in {
|
||||
PacketCoding.decodePacket(OpolE).require match {
|
||||
case OutfitMemberEvent(outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, unk0_padding)) =>
|
||||
outfit_id mustEqual 6418
|
||||
member_id mustEqual 42644970
|
||||
member_name mustEqual "OpolE"
|
||||
rank mustEqual 6
|
||||
points mustEqual 461901
|
||||
last_login mustEqual 137576
|
||||
action mustEqual OutfitMemberEventAction.PacketType.Padding
|
||||
unk0_padding mustEqual 0
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode OpolE padding" in {
|
||||
val msg = OutfitMemberEvent(
|
||||
outfit_id = 6418,
|
||||
member_id = 42644970,
|
||||
Unk0(
|
||||
member_name = "OpolE",
|
||||
rank = 6,
|
||||
points = 461901,
|
||||
last_online = 137576,
|
||||
action = OutfitMemberEventAction.PacketType.Padding,
|
||||
padding = 0
|
||||
)
|
||||
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual OpolE
|
||||
}
|
||||
|
||||
"decode Unk1" in {
|
||||
PacketCoding.decodePacket(unk1).require match {
|
||||
case OutfitMemberEvent(outfit_id, member_id, Unk1()) =>
|
||||
outfit_id mustEqual 529744
|
||||
member_id mustEqual 41605263
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Unk1" in {
|
||||
val msg = OutfitMemberEvent(
|
||||
outfit_id = 529744,
|
||||
member_id = 41605263,
|
||||
Unk1()
|
||||
)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk1
|
||||
}
|
||||
}
|
||||
51
src/test/scala/game/OutfitMemberUpdateTest.scala
Normal file
51
src/test/scala/game/OutfitMemberUpdateTest.scala
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) 2023-2025 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.OutfitMemberUpdate
|
||||
import org.specs2.mutable._
|
||||
import scodec.bits._
|
||||
|
||||
class OutfitMemberUpdateTest extends Specification {
|
||||
|
||||
val updateRankToOwnerOfOutfitInFormation = hex"91 0100ff7f15aa7a02f0"
|
||||
val normalRankChange = hex"91 1219000086d9130090"
|
||||
|
||||
"decode updateOwnerOfOutfitInFormation" in {
|
||||
PacketCoding.decodePacket(updateRankToOwnerOfOutfitInFormation).require match {
|
||||
case OutfitMemberUpdate(outfit_id, char_id, rank, flag) =>
|
||||
outfit_id mustEqual 2147418113
|
||||
char_id mustEqual 41593365
|
||||
rank mustEqual 7
|
||||
flag mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode updateOwnerOfOutfitInFormation" in {
|
||||
val msg = OutfitMemberUpdate(outfit_id = 2147418113, char_id = 41593365, rank = 7, flag = true)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual updateRankToOwnerOfOutfitInFormation
|
||||
}
|
||||
|
||||
"decode normalRankChange" in {
|
||||
PacketCoding.decodePacket(normalRankChange).require match {
|
||||
case OutfitMemberUpdate(outfit_id, char_id, rank, flag) =>
|
||||
outfit_id mustEqual 6418
|
||||
char_id mustEqual 1300870
|
||||
rank mustEqual 4
|
||||
flag mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode normalRankChange" in {
|
||||
val msg = OutfitMemberUpdate(outfit_id = 6418, char_id = 1300870, rank = 4, flag = true)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual normalRankChange
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
// Copyright (c) 2023-2025 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.OutfitAction.{AcceptOutfitInvite, CancelOutfitInvite, CreateOutfit, FormOutfit, RejectOutfitInvite}
|
||||
import net.psforever.packet.game.OutfitMembershipRequest.RequestType
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import net.psforever.packet.game.OutfitMembershipRequest
|
||||
import net.psforever.packet.game.OutfitMembershipRequestAction._
|
||||
import org.specs2.mutable._
|
||||
import scodec.bits._
|
||||
|
||||
class OutfitMembershipRequestTest extends Specification {
|
||||
|
||||
val create_ABC = hex"8c 0 0200 000 1000 83 410042004300"
|
||||
val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200"
|
||||
val form_abc = hex"8c 2 0200 000 1000 83 610062006300"
|
||||
val form_1 = hex"8c 2 1000 000 1000 81 3100"
|
||||
val unk3 = hex"8c 5 bb39 9e0 2000 0000 1080 750072006f006200" // -- "urob" -- could be false positive -- seems to gets an OMSResp -> 0x8d271bb399e025af8f405080550072006f0062008080
|
||||
val invite_old = hex"8c 5 bb399e0 2000 0000 1140 7600690072007500730067006900760065007200" // -- virusgiver
|
||||
val unk3 = hex"8c 5 bb399e0 2000 0000 1080 750072006f006200" // -- "urob" -- could be false positive -- seems to gets an OMSResp -> 0x8d271bb399e025af8f405080550072006f0062008080
|
||||
val accept_1 = hex"8c 6 0200 000 1000"
|
||||
val accept_2 = hex"8c 6 0400 000 1000"
|
||||
val reject_1 = hex"8c 8 0200 000 1000"
|
||||
|
|
@ -23,20 +23,24 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
val cancel_1_abc = hex"8c a 0200 000 0000 0000 1060 610062006300"
|
||||
val cancel_3_def = hex"8c a 0600 000 0000 0000 1060 640065006600" // /outfitcancel 123 def -- first parameter is skipped
|
||||
|
||||
// dumped from half implemented outfit
|
||||
val invite = hex"8c4020000000000000116069006e00760069007400650054006500730074003100"
|
||||
val kick = hex"8cc020000017ac8f405000"
|
||||
val setrank = hex"8ce020000017ac8f404600" // setting rank from 0 to 1
|
||||
|
||||
"decode CreateOutfit ABC" in {
|
||||
PacketCoding.decodePacket(create_ABC).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Create
|
||||
avatar_id mustEqual PlanetSideGUID(1)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual CreateOutfit("", 0, unk4 = false, "ABC")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
|
||||
outfit_id mustEqual 1
|
||||
action mustEqual Create("", "ABC")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode CreateOutfit ABC" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(1), 0, CreateOutfit("", 0, unk4 = false, "ABC"))
|
||||
val msg = OutfitMembershipRequest(1, Create("", "ABC"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual create_ABC
|
||||
|
|
@ -44,18 +48,16 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode CreateOutfit 2222" in {
|
||||
PacketCoding.decodePacket(create_2222).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Create
|
||||
avatar_id mustEqual PlanetSideGUID(8)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual CreateOutfit("", 0, unk4 = false, "2222")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 8
|
||||
action mustEqual Create("", "2222")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode CreateOutfit 2222" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(8), 0, CreateOutfit("", 0, unk4 = false, "2222"))
|
||||
val msg = OutfitMembershipRequest(8, Create("", "2222"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual create_2222
|
||||
|
|
@ -63,18 +65,16 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode FormOutfit abc" in {
|
||||
PacketCoding.decodePacket(form_abc).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Form
|
||||
avatar_id mustEqual PlanetSideGUID(1)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual FormOutfit("", 0, unk4 = false, "abc")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 1
|
||||
action mustEqual Form("", "abc")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode FormOutfit abc" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(1), 0, FormOutfit("", 0, unk4 = false, "abc"))
|
||||
val msg = OutfitMembershipRequest(1, Form("", "abc"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual form_abc
|
||||
|
|
@ -82,37 +82,50 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode FormOutfit 1" in {
|
||||
PacketCoding.decodePacket(form_1).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Form
|
||||
avatar_id mustEqual PlanetSideGUID(8)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual FormOutfit("", 0, unk4 = false, "1")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 8
|
||||
action mustEqual Form("", "1")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode FormOutfit 1" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(8), 0, FormOutfit("", 0, unk4 = false, "1"))
|
||||
val msg = OutfitMembershipRequest(8, Form("", "1"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual form_1
|
||||
}
|
||||
|
||||
"decode Invite" in {
|
||||
PacketCoding.decodePacket(invite_old).require match {
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 30383325L
|
||||
action mustEqual Invite(0, "virusgiver")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Invite" in {
|
||||
val msg = OutfitMembershipRequest(30383325L, Invite(0, "virusgiver"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual invite_old
|
||||
}
|
||||
|
||||
"decode AcceptOutfitInvite 1" in {
|
||||
PacketCoding.decodePacket(accept_1).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Accept
|
||||
avatar_id mustEqual PlanetSideGUID(1)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual AcceptOutfitInvite("")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 1
|
||||
action mustEqual AcceptInvite("")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode AcceptOutfitInvite 1" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Accept, PlanetSideGUID(1), 0, AcceptOutfitInvite(""))
|
||||
val msg = OutfitMembershipRequest(1, AcceptInvite(""))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual accept_1
|
||||
|
|
@ -120,18 +133,16 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode AcceptOutfitInvite 2" in {
|
||||
PacketCoding.decodePacket(accept_2).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Accept
|
||||
avatar_id mustEqual PlanetSideGUID(2)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual AcceptOutfitInvite("")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 2
|
||||
action mustEqual AcceptInvite("")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode AcceptOutfitInvite 2" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Accept, PlanetSideGUID(2), 0, AcceptOutfitInvite(""))
|
||||
val msg = OutfitMembershipRequest(2, AcceptInvite(""))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual accept_2
|
||||
|
|
@ -139,18 +150,16 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode RejectOutfitInvite 1" in {
|
||||
PacketCoding.decodePacket(reject_1).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Reject
|
||||
avatar_id mustEqual PlanetSideGUID(1)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual RejectOutfitInvite("")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 1
|
||||
action mustEqual RejectInvite("")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode RejectOutfitInvite 1" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Reject, PlanetSideGUID(1), 0, RejectOutfitInvite(""))
|
||||
val msg = OutfitMembershipRequest(1, RejectInvite(""))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual reject_1
|
||||
|
|
@ -158,18 +167,16 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode RejectOutfitInvite 2" in {
|
||||
PacketCoding.decodePacket(reject_2).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Reject
|
||||
avatar_id mustEqual PlanetSideGUID(2)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual RejectOutfitInvite("")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 2
|
||||
action mustEqual RejectInvite("")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode RejectOutfitInvite 2" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Reject, PlanetSideGUID(2), 0, RejectOutfitInvite(""))
|
||||
val msg = OutfitMembershipRequest(2, RejectInvite(""))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual reject_2
|
||||
|
|
@ -177,18 +184,16 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode CancelOutfitInvite 3" in {
|
||||
PacketCoding.decodePacket(cancel_3).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Cancel
|
||||
avatar_id mustEqual PlanetSideGUID(3)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual CancelOutfitInvite(0, 0, "")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 3
|
||||
action mustEqual CancelInvite(0, "")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode CancelOutfitInvite 3" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(3), 0, CancelOutfitInvite(0, 0, ""))
|
||||
val msg = OutfitMembershipRequest(3, CancelInvite(0, ""))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual cancel_3
|
||||
|
|
@ -196,18 +201,16 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode CancelOutfitInvite 1 abc" in {
|
||||
PacketCoding.decodePacket(cancel_1_abc).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Cancel
|
||||
avatar_id mustEqual PlanetSideGUID(1)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual CancelOutfitInvite(0, 0, "abc")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 1
|
||||
action mustEqual CancelInvite(0, "abc")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode CancelOutfitInvite 1 abc" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(1), 0, CancelOutfitInvite(0, 0, "abc"))
|
||||
val msg = OutfitMembershipRequest(1, CancelInvite(0, "abc"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual cancel_1_abc
|
||||
|
|
@ -215,20 +218,77 @@ class OutfitMembershipRequestTest extends Specification {
|
|||
|
||||
"decode CancelOutfitInvite 3 def" in {
|
||||
PacketCoding.decodePacket(cancel_3_def).require match {
|
||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
||||
request_type mustEqual RequestType.Cancel
|
||||
avatar_id mustEqual PlanetSideGUID(3)
|
||||
unk1 mustEqual 0
|
||||
action mustEqual CancelOutfitInvite(0, 0, "def")
|
||||
case OutfitMembershipRequest(outfit_id, action) =>
|
||||
outfit_id mustEqual 3
|
||||
action mustEqual CancelInvite(0, "def")
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode CancelOutfitInvite 3 def" in {
|
||||
val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(3), 0, CancelOutfitInvite(0, 0, "def"))
|
||||
val msg = OutfitMembershipRequest(3, CancelInvite(0, "def"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual cancel_3_def
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
"decode invite" in {
|
||||
PacketCoding.decodePacket(invite).require match {
|
||||
case OutfitMembershipRequest(outfit_id, Invite(unk1, member_name)) =>
|
||||
outfit_id mustEqual 1
|
||||
unk1 mustEqual 0
|
||||
member_name mustEqual "inviteTest1"
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode invite" in {
|
||||
val msg = OutfitMembershipRequest(1, Invite(0, "inviteTest1"))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual invite
|
||||
}
|
||||
|
||||
|
||||
"decode kick" in {
|
||||
PacketCoding.decodePacket(kick).require match {
|
||||
case OutfitMembershipRequest(outfit_id, Kick(avatar_id, member_name)) =>
|
||||
outfit_id mustEqual 1
|
||||
avatar_id mustEqual 41575613
|
||||
member_name mustEqual ""
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode kick" in {
|
||||
val msg = OutfitMembershipRequest(1, Kick(41575613, ""))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual kick
|
||||
}
|
||||
|
||||
"decode setrank" in {
|
||||
PacketCoding.decodePacket(setrank).require match {
|
||||
case OutfitMembershipRequest(outfit_id, SetRank(avatar_id, rank, member_name)) =>
|
||||
outfit_id mustEqual 1
|
||||
avatar_id mustEqual 41575613
|
||||
rank mustEqual 1
|
||||
member_name mustEqual ""
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode setrank" in {
|
||||
val msg = OutfitMembershipRequest(1, SetRank(41575613, 1, ""))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual setrank
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
156
src/test/scala/game/OutfitMembershipResponseTest.scala
Normal file
156
src/test/scala/game/OutfitMembershipResponseTest.scala
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (c) 2023-2025 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.OutfitMembershipResponse
|
||||
import net.psforever.packet.game.OutfitMembershipResponse.PacketType
|
||||
import org.specs2.mutable._
|
||||
import scodec.bits._
|
||||
|
||||
class OutfitMembershipResponseTest extends Specification {
|
||||
|
||||
val createResponse = hex"8d 0 002b54f404000000010008080"
|
||||
val unk1 = hex"8d 2 01bb399e03ddb4f4050a078004e00690063006b009550006c0061006e006500740053006900640065005f0046006f00720065007600650072005f005400520000"
|
||||
val unk2 = hex"8d 4 0049b0f4042b54f4051405a006500720067006c0069006e006700390032009750006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e00750000"
|
||||
val unk3 = hex"8d 6 00e8c2f40510d3b6030008080"
|
||||
val unk4 = hex"8d 8 002b54f404000000010008080"
|
||||
val unk5 = hex"8d a 022b54f4051fb0f4051c05000530046006f0075007400660069007400740065007300740031008080"
|
||||
|
||||
"decode CreateResponse" in {
|
||||
PacketCoding.decodePacket(createResponse).require match {
|
||||
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
|
||||
packet_type mustEqual PacketType.CreateResponse
|
||||
unk0 mustEqual 0
|
||||
unk1 mustEqual 0
|
||||
outfit_id mustEqual 41593365
|
||||
target_id mustEqual 0
|
||||
str1 mustEqual ""
|
||||
str2 mustEqual ""
|
||||
flag mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode CreateResponse" in {
|
||||
val msg = OutfitMembershipResponse(PacketType.CreateResponse, 0, 0, 41593365, 0, "", "", flag = true)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual createResponse
|
||||
}
|
||||
|
||||
"decode unk1" in {
|
||||
PacketCoding.decodePacket(unk1).require match {
|
||||
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
|
||||
packet_type mustEqual PacketType.Invite
|
||||
unk0 mustEqual 0
|
||||
unk1 mustEqual 0
|
||||
outfit_id mustEqual 30383325
|
||||
target_id mustEqual 41605870
|
||||
str1 mustEqual "xNick"
|
||||
str2 mustEqual "PlanetSide_Forever_TR"
|
||||
flag mustEqual false
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode unk1" in {
|
||||
val msg = OutfitMembershipResponse(PacketType.Invite, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk1
|
||||
}
|
||||
|
||||
"decode unk2" in {
|
||||
PacketCoding.decodePacket(unk2).require match {
|
||||
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
|
||||
packet_type mustEqual PacketType.InviteAccepted
|
||||
unk0 mustEqual 0
|
||||
unk1 mustEqual 0
|
||||
outfit_id mustEqual 41605156
|
||||
target_id mustEqual 41593365
|
||||
str1 mustEqual "Zergling92"
|
||||
str2 mustEqual "PlanetSide_Forever_Vanu"
|
||||
flag mustEqual false
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode unk2" in {
|
||||
val msg = OutfitMembershipResponse(PacketType.InviteAccepted, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk2
|
||||
}
|
||||
|
||||
"decode unk3" in {
|
||||
PacketCoding.decodePacket(unk3).require match {
|
||||
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
|
||||
packet_type mustEqual PacketType.InviteRejected
|
||||
unk0 mustEqual 0
|
||||
unk1 mustEqual 0
|
||||
outfit_id mustEqual 41574772
|
||||
target_id mustEqual 31156616
|
||||
str1 mustEqual ""
|
||||
str2 mustEqual ""
|
||||
flag mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode unk3" in {
|
||||
val msg = OutfitMembershipResponse(PacketType.InviteRejected, 0, 0, 41574772, 31156616, "", "", flag = true)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk3
|
||||
}
|
||||
|
||||
"decode unk4" in {
|
||||
PacketCoding.decodePacket(unk4).require match {
|
||||
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
|
||||
packet_type mustEqual PacketType.Unk4
|
||||
unk0 mustEqual 0
|
||||
unk1 mustEqual 0
|
||||
outfit_id mustEqual 41593365
|
||||
target_id mustEqual 0
|
||||
str1 mustEqual ""
|
||||
str2 mustEqual ""
|
||||
flag mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode unk4" in {
|
||||
val msg = OutfitMembershipResponse(PacketType.Unk4, 0, 0, 41593365, 0, "", "", flag = true)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk4
|
||||
}
|
||||
|
||||
"decode unk5" in {
|
||||
PacketCoding.decodePacket(unk5).require match {
|
||||
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
|
||||
packet_type mustEqual PacketType.Kick
|
||||
unk0 mustEqual 0
|
||||
unk1 mustEqual 1
|
||||
outfit_id mustEqual 41593365
|
||||
target_id mustEqual 41605263
|
||||
str1 mustEqual "PSFoutfittest1"
|
||||
str2 mustEqual ""
|
||||
flag mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode unk5" in {
|
||||
val msg = OutfitMembershipResponse(PacketType.Kick, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual unk5
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import scodec.bits._
|
||||
|
||||
class OutfitRequestTest extends Specification {
|
||||
val string0 = hex"8e02b54f40401780560061006e00750020006f0075007400660069007400200066006f0072002000740068006500200070006c0061006e00650074007300690064006500200066006f00720065007600650072002000700072006f006a006500630074002100200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002d00660069006e00640020006f007500740020006d006f00720065002000610062006f0075007400200074006800650020005000530045004d0055002000700072006f006a0065006300740020006100740020005000530066006f00720065007600650072002e006e0065007400"
|
||||
val string2 = hex"8e22b54f405800c000c000c000c000c000c000c000"
|
||||
val string4 = hex"8e42b54f404aa0" //faked by modifying the previous example
|
||||
val string6 = hex"8e649e822010"
|
||||
val string8 = hex"8e81b2cf4050"
|
||||
|
||||
"decode 0" in {
|
||||
PacketCoding.decodePacket(string0).require match {
|
||||
case OutfitRequest(id, OutfitRequestForm.Unk0(str)) =>
|
||||
id mustEqual 41593365L
|
||||
str mustEqual "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net"
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode 1" in {
|
||||
PacketCoding.decodePacket(string2).require match {
|
||||
case OutfitRequest(id, OutfitRequestForm.Unk1(list)) =>
|
||||
id mustEqual 41593365L
|
||||
list mustEqual List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""))
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode 2 (fake)" in {
|
||||
PacketCoding.decodePacket(string4).require match {
|
||||
case OutfitRequest(id, OutfitRequestForm.Unk2(value)) =>
|
||||
id mustEqual 41593365L
|
||||
value mustEqual 85
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode 3" in {
|
||||
PacketCoding.decodePacket(string6).require match {
|
||||
case OutfitRequest(id, OutfitRequestForm.Unk3(value)) =>
|
||||
id mustEqual 1176612L
|
||||
value mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode 4" in {
|
||||
PacketCoding.decodePacket(string8).require match {
|
||||
case OutfitRequest(id, OutfitRequestForm.Unk4(value)) =>
|
||||
id mustEqual 41588237L
|
||||
value mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode 0" in {
|
||||
val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk0(
|
||||
"Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net"
|
||||
))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string0
|
||||
}
|
||||
|
||||
"encode 1" in {
|
||||
val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk1(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""))))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string2
|
||||
}
|
||||
|
||||
"encode 2 (fake)" in {
|
||||
val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk2(85))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string4
|
||||
}
|
||||
|
||||
"encode 3" in {
|
||||
val msg = OutfitRequest(1176612L, OutfitRequestForm.Unk3(true))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string6
|
||||
}
|
||||
|
||||
"encode 4" in {
|
||||
val msg = OutfitRequest(41588237L, OutfitRequestForm.Unk4(true))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string8
|
||||
}
|
||||
}
|
||||
103
src/test/scala/game/OutfitRequestTest.scala
Normal file
103
src/test/scala/game/OutfitRequestTest.scala
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright (c) 2023-2025 PSForever
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.{OutfitRequest, OutfitRequestAction}
|
||||
import scodec.bits._
|
||||
|
||||
class OutfitRequestTest extends Specification {
|
||||
|
||||
val setMotd = hex"8e 02b54f40401780560061006e00750020006f0075007400660069007400200066006f0072002000740068006500200070006c0061006e00650074007300690064006500200066006f00720065007600650072002000700072006f006a006500630074002100200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002d00660069006e00640020006f007500740020006d006f00720065002000610062006f0075007400200074006800650020005000530045004d0055002000700072006f006a0065006300740020006100740020005000530066006f00720065007600650072002e006e0065007400"
|
||||
val setRanks = hex"8e 22b54f405800c000c000c000c000c000c000c000"
|
||||
val string4 = hex"8e 42b54f404aa0" //faked by modifying the previous example
|
||||
val string6 = hex"8e 649e822010"
|
||||
val string8 = hex"8e 81b2cf4050"
|
||||
|
||||
"decode Motd" in {
|
||||
PacketCoding.decodePacket(setMotd).require match {
|
||||
case OutfitRequest(id, OutfitRequestAction.Motd(str)) =>
|
||||
id mustEqual 41593365L
|
||||
str mustEqual "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net"
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode Ranks" in {
|
||||
PacketCoding.decodePacket(setRanks).require match {
|
||||
case OutfitRequest(id, OutfitRequestAction.Ranks(list)) =>
|
||||
id mustEqual 41593365L
|
||||
list mustEqual List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""))
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode Unk2 (fake)" in {
|
||||
PacketCoding.decodePacket(string4).require match {
|
||||
case OutfitRequest(id, OutfitRequestAction.Unk2(value)) =>
|
||||
id mustEqual 41593365L
|
||||
value mustEqual 85
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode Unk3" in {
|
||||
PacketCoding.decodePacket(string6).require match {
|
||||
case OutfitRequest(id, OutfitRequestAction.Unk3(value)) =>
|
||||
id mustEqual 1176612L
|
||||
value mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode Unk4" in {
|
||||
PacketCoding.decodePacket(string8).require match {
|
||||
case OutfitRequest(id, OutfitRequestAction.Unk4(value)) =>
|
||||
id mustEqual 41588237L
|
||||
value mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode Motd" in {
|
||||
val msg = OutfitRequest(41593365L, OutfitRequestAction.Motd(
|
||||
"Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net"
|
||||
))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual setMotd
|
||||
}
|
||||
|
||||
"encode Ranks" in {
|
||||
val msg = OutfitRequest(41593365L, OutfitRequestAction.Ranks(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""))))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual setRanks
|
||||
}
|
||||
|
||||
"encode Unk2 (fake)" in {
|
||||
val msg = OutfitRequest(41593365L, OutfitRequestAction.Unk2(85))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string4
|
||||
}
|
||||
|
||||
"encode Unk3" in {
|
||||
val msg = OutfitRequest(1176612L, OutfitRequestAction.Unk3(true))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string6
|
||||
}
|
||||
|
||||
"encode Unk4" in {
|
||||
val msg = OutfitRequest(41588237L, OutfitRequestAction.Unk4(true))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string8
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2019 PSForever
|
||||
// Copyright (c) 2019-2025 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.packet._
|
||||
|
|
@ -11,14 +11,14 @@ class SquadMemberEventTest extends Specification {
|
|||
|
||||
"decode" in {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) =>
|
||||
u1 mustEqual MemberEvent.Add
|
||||
case SquadMemberEvent(event, u2, char_id, position, player_name, zone_number, outfit_id) =>
|
||||
event mustEqual MemberEvent.Add
|
||||
u2 mustEqual 7
|
||||
u3 mustEqual 42771010L
|
||||
u4 mustEqual 0
|
||||
u5.contains("HofD") mustEqual true
|
||||
u6.contains(7) mustEqual true
|
||||
u7.contains(529745L) mustEqual true
|
||||
char_id mustEqual 42771010L
|
||||
position mustEqual 0
|
||||
player_name.contains("HofD") mustEqual true
|
||||
zone_number.contains(7) mustEqual true
|
||||
outfit_id.contains(529745L) mustEqual true
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue