mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-14 07:55:07 +00:00
Merge pull request #1304 from psforever/outfit
Outfit packets and handling
This commit is contained in:
commit
cfb5c0def2
41 changed files with 3116 additions and 401 deletions
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
|
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)
|
* 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)
|
* 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] = {
|
def loadExperienceDebt(avatarId: Long): Future[Long] = {
|
||||||
import ctx._
|
import ctx._
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
@ -1737,6 +1755,12 @@ class AvatarActor(
|
||||||
case AwardCep(cep) =>
|
case AwardCep(cep) =>
|
||||||
if (experienceDebt == 0L) {
|
if (experienceDebt == 0L) {
|
||||||
setCep(avatar.cep + cep)
|
setCep(avatar.cep + cep)
|
||||||
|
if (session.get.player.outfit_id != 0) {
|
||||||
|
setOutfitPoints(avatar.id.toLong, cep * 2).onComplete {
|
||||||
|
case Success(_) =>
|
||||||
|
case Failure(exception) => log.error(exception)("db failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (cep > 0) {
|
} else if (cep > 0) {
|
||||||
sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage())
|
sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage())
|
||||||
}
|
}
|
||||||
|
|
@ -2994,6 +3018,13 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
avatar = avatar.copy(bep = newBep, implants = implants)
|
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) =>
|
case Failure(exception) =>
|
||||||
log.error(exception)("db failure")
|
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.serverobject.terminals.{ProximityUnit, Terminal}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.packet.PlanetSideGamePacket
|
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.{InterstellarClusterService => ICS}
|
||||||
import net.psforever.services.CavernRotationService
|
import net.psforever.services.CavernRotationService
|
||||||
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
|
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
|
||||||
|
|
@ -610,7 +610,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
case packet: HitHint =>
|
case packet: HitHint =>
|
||||||
logic.general.handleHitHint(packet)
|
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 =>
|
case pkt =>
|
||||||
data.log.warn(s"Unhandled GamePacket $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.vital.Vitality
|
||||||
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
|
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
|
||||||
import net.psforever.packet.PlanetSideGamePacket
|
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, Initial, 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.RemoverActor
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||||
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||||
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
import scala.util.Success
|
import scala.util.Success
|
||||||
|
|
||||||
|
|
@ -665,6 +667,19 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
||||||
val HitHint(_, _) = pkt
|
val HitHint(_, _) = pkt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {}
|
||||||
|
|
||||||
|
def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {}
|
||||||
|
|
||||||
|
def handleOutfitRequest(pkt: OutfitRequest): Unit = {
|
||||||
|
pkt match {
|
||||||
|
case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(true)) =>
|
||||||
|
|
||||||
|
case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(false)) =>
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
/* messages */
|
/* messages */
|
||||||
|
|
||||||
def handleRenewCharSavedTimer(): Unit = { /* */ }
|
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.serverobject.mount.Mountable
|
||||||
import net.psforever.objects.sourcing.PlayerSource
|
import net.psforever.objects.sourcing.PlayerSource
|
||||||
import net.psforever.objects.vital.interaction.Adversarial
|
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.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
import net.psforever.types.ImplantType
|
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 =>
|
case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget =>
|
||||||
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
|
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
|
||||||
|
|
||||||
|
case AvatarResponse.PlanetsideStringAttribute(attributeType, attributeValue) =>
|
||||||
|
sendResponse(PlanetsideStringAttributeMessage(guid, attributeType, attributeValue))
|
||||||
|
|
||||||
case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
|
case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
|
||||||
sendResponse(GenericObjectActionMessage(objectGuid, actionCode))
|
sendResponse(GenericObjectActionMessage(objectGuid, actionCode))
|
||||||
|
|
||||||
|
|
@ -476,6 +479,12 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
|
||||||
case AvatarResponse.ShareKillExperienceWithSquad(killer, exp) =>
|
case AvatarResponse.ShareKillExperienceWithSquad(killer, exp) =>
|
||||||
ops.shareKillExperienceWithSquad(killer, exp)
|
ops.shareKillExperienceWithSquad(killer, exp)
|
||||||
|
|
||||||
|
case AvatarResponse.ShareAntExperienceWithSquad(owner, exp, vehicle) =>
|
||||||
|
ops.shareAntExperienceWithSquad(owner, exp, vehicle)
|
||||||
|
|
||||||
|
case AvatarResponse.RemoveFromOutfitChat(outfit_id) =>
|
||||||
|
ops.removeFromOutfitChat(outfit_id)
|
||||||
|
|
||||||
case AvatarResponse.SendResponse(msg) =>
|
case AvatarResponse.SendResponse(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import net.psforever.actors.session.spectator.SpectatorMode
|
||||||
import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
|
import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
|
||||||
import net.psforever.objects.Session
|
import net.psforever.objects.Session
|
||||||
import net.psforever.packet.game.{ChatMsg, ServerType, SetChatFilterMessage}
|
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
|
||||||
import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM}
|
import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM}
|
||||||
import net.psforever.util.Config
|
import net.psforever.util.Config
|
||||||
|
|
@ -79,6 +79,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
|
||||||
case (CMT_SQUAD, _, _) =>
|
case (CMT_SQUAD, _, _) =>
|
||||||
ops.commandSquad(session, message, SquadChannel(sessionLogic.squad.squad_guid))
|
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, _, _) =>
|
case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
|
||||||
ops.commandWho(session)
|
ops.commandWho(session)
|
||||||
|
|
||||||
|
|
@ -100,7 +103,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
|
||||||
def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
|
def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
|
||||||
import ChatMessageType._
|
import ChatMessageType._
|
||||||
message.messageType match {
|
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)
|
ops.commandIncomingSendAllIfOnline(session, message)
|
||||||
|
|
||||||
case CMT_OPEN =>
|
case CMT_OPEN =>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ package net.psforever.actors.session.normal
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import akka.actor.{ActorContext, ActorRef, typed}
|
import akka.actor.{ActorContext, ActorRef, typed}
|
||||||
import net.psforever.actors.session.{AvatarActor, SessionActor}
|
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.{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.avatar.{Avatar, PlayerControl, SpecialCarry}
|
||||||
import net.psforever.objects.ballistics.Projectile
|
import net.psforever.objects.ballistics.Projectile
|
||||||
|
|
@ -37,13 +37,14 @@ import net.psforever.objects.vital.etc.SuicideReason
|
||||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||||
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
|
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
|
||||||
import net.psforever.packet.PlanetSideGamePacket
|
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.{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, 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.account.{AccountPersistenceService, RetrieveAccountData}
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||||
import net.psforever.services.local.support.CaptureFlagManager
|
import net.psforever.services.local.support.CaptureFlagManager
|
||||||
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||||
import net.psforever.util.Config
|
import net.psforever.util.Config
|
||||||
|
import net.psforever.zones.Zones.zones
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
|
@ -796,6 +797,65 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
||||||
val HitHint(_, _) = pkt
|
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)) =>
|
||||||
|
if (player.outfit_id != 0) {
|
||||||
|
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, _)) =>
|
||||||
|
if (player.outfit_id != 0) {
|
||||||
|
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))) =>
|
||||||
|
SessionOutfitHandlers.HandleOutfitRank(zones, List(r1, r2, r3, r4, r5, r6, r7, r8), player)
|
||||||
|
|
||||||
|
case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(true)) =>
|
||||||
|
player.outfit_window_open = true
|
||||||
|
SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id)
|
||||||
|
|
||||||
|
case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(false)) =>
|
||||||
|
player.outfit_window_open = false
|
||||||
|
|
||||||
|
case OutfitRequest(_, OutfitRequestAction.OutfitListWindowOpen(true)) =>
|
||||||
|
player.outfit_list_open = true
|
||||||
|
SessionOutfitHandlers.HandleGetOutfitList(player)
|
||||||
|
|
||||||
|
case OutfitRequest(_, OutfitRequestAction.OutfitListWindowOpen(false)) =>
|
||||||
|
player.outfit_list_open = false
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* messages */
|
/* messages */
|
||||||
|
|
||||||
def handleRenewCharSavedTimer(): Unit = {
|
def handleRenewCharSavedTimer(): Unit = {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.doors.Door
|
||||||
import net.psforever.objects.vehicles.Utility
|
import net.psforever.objects.vehicles.Utility
|
||||||
import net.psforever.objects.zones.ZoneProjectile
|
import net.psforever.objects.zones.ZoneProjectile
|
||||||
import net.psforever.packet.PlanetSideGamePacket
|
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.account.AccountPersistenceService
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
import net.psforever.types.{ExoSuitType, Vector3}
|
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 handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ }
|
||||||
|
|
||||||
|
def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {}
|
||||||
|
|
||||||
|
def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {}
|
||||||
|
|
||||||
|
def handleOutfitRequest(pkt: OutfitRequest): Unit = {}
|
||||||
|
|
||||||
/* messages */
|
/* messages */
|
||||||
|
|
||||||
def handleRenewCharSavedTimer(): Unit = { /* intentionally blank */ }
|
def handleRenewCharSavedTimer(): Unit = { /* intentionally blank */ }
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import net.psforever.objects.LivePlayerList
|
||||||
import net.psforever.objects.sourcing.PlayerSource
|
import net.psforever.objects.sourcing.PlayerSource
|
||||||
import net.psforever.objects.zones.ZoneInfo
|
import net.psforever.objects.zones.ZoneInfo
|
||||||
import net.psforever.packet.game.SetChatFilterMessage
|
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.local.{LocalAction, LocalServiceMessage}
|
||||||
import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse}
|
import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse}
|
||||||
import net.psforever.types.ChatMessageType.CMT_QUIT
|
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 = {
|
def commandWho(session: Session): Unit = {
|
||||||
val players = session.zone.Players
|
val players = session.zone.Players
|
||||||
val popTR = players.count(_.faction == PlanetSideEmpire.TR)
|
val popTR = players.count(_.faction == PlanetSideEmpire.TR)
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,12 @@ trait GeneralFunctions extends CommonSessionInterfacingFunctionality {
|
||||||
def handleCanNotPutItemInSlot(msg: Containable.CanNotPutItemInSlot): Unit
|
def handleCanNotPutItemInSlot(msg: Containable.CanNotPutItemInSlot): Unit
|
||||||
|
|
||||||
def handleReceiveDefaultMessage(default: Any, sender: ActorRef): 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(
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,12 +3,13 @@ package net.psforever.actors.session.support
|
||||||
|
|
||||||
import akka.actor.{ActorContext, typed}
|
import akka.actor.{ActorContext, typed}
|
||||||
import net.psforever.objects.serverobject.mount.Mountable
|
import net.psforever.objects.serverobject.mount.Mountable
|
||||||
import net.psforever.objects.{Default, PlanetSideGameObject, Player}
|
import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle}
|
||||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, UniquePlayer}
|
||||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||||
import net.psforever.objects.zones.exp
|
import net.psforever.objects.zones.exp
|
||||||
import net.psforever.services.Service
|
import net.psforever.services.Service
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage, AvatarServiceResponse}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage, AvatarServiceResponse}
|
||||||
|
import net.psforever.services.chat.OutfitChannel
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
//
|
//
|
||||||
|
|
@ -145,6 +146,21 @@ class SessionAvatarHandlers(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def shareAntExperienceWithSquad(driver: UniquePlayer, exp: Long, vehicle: Vehicle): Unit = {
|
||||||
|
val squadUI = sessionLogic.squad.squadUI
|
||||||
|
val squadSize = squadUI.size
|
||||||
|
if (squadSize > 1) {
|
||||||
|
val squadMembers = squadUI.filterNot(_._1 == driver.charId).map { case (_, member) => member }.toList.map(_.name)
|
||||||
|
val playersInZone = vehicle.Zone.Players.map { avatar => (avatar.id, avatar.basic.name) }
|
||||||
|
val squadMembersHere = playersInZone.filter(member => squadMembers.contains(member._2))
|
||||||
|
squadMembersHere.foreach { member =>
|
||||||
|
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
member._2,
|
||||||
|
AvatarAction.AwardBep(member._1, exp, ExperienceType.Normal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properly format a `DestroyDisplayMessage` packet
|
* Properly format a `DestroyDisplayMessage` packet
|
||||||
* given sufficient information about a target (victim) and an actor (killer).
|
* given sufficient information about a target (victim) and an actor (killer).
|
||||||
|
|
@ -222,6 +238,10 @@ class SessionAvatarHandlers(
|
||||||
}
|
}
|
||||||
player.VehicleSeated = None
|
player.VehicleSeated = None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def removeFromOutfitChat(outfit_id: Long): Unit = {
|
||||||
|
sessionLogic.chat.LeaveChannel(OutfitChannel(outfit_id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object SessionAvatarHandlers {
|
object SessionAvatarHandlers {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,874 @@
|
||||||
|
// Copyright (c) 2025 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import io.getquill.{Action, ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase}
|
||||||
|
import net.psforever.objects.avatar.PlayerControl
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.objects.Player
|
||||||
|
import net.psforever.packet.game.OutfitEventAction.{Initial, Leaving, OutfitInfo, OutfitRankNames, Unk1, Update, UpdateMemberCount}
|
||||||
|
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 java.util.concurrent.Executors
|
||||||
|
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, Update(
|
||||||
|
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, UpdateMemberCount(memberCount)))
|
||||||
|
|
||||||
|
PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name,
|
||||||
|
OutfitMemberEvent(outfitId, invited.CharId,
|
||||||
|
OutfitMemberEventAction.Update(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, Initial(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) {
|
||||||
|
|
||||||
|
// store outfit_id since it will be nulled soon
|
||||||
|
val outfit_id = kickedBy.outfit_id
|
||||||
|
|
||||||
|
removeMemberFromOutfit(outfit_id, kickedId).map {
|
||||||
|
case (deleted, _) =>
|
||||||
|
if (deleted > 0) {
|
||||||
|
|
||||||
|
PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name,
|
||||||
|
OutfitEvent(outfit_id, Leaving())
|
||||||
|
)
|
||||||
|
|
||||||
|
session.chat.LeaveChannel(OutfitChannel(outfit_id))
|
||||||
|
kickedBy.outfit_name = ""
|
||||||
|
kickedBy.outfit_id = 0
|
||||||
|
|
||||||
|
zones.filter(z => z.AllPlayers.nonEmpty).flatMap(_.AllPlayers)
|
||||||
|
.filter(p => p.outfit_id == outfit_id).foreach(outfitMember =>
|
||||||
|
PlayerControl.sendResponse(outfitMember.Zone, outfitMember.Name,
|
||||||
|
OutfitMemberEvent(outfit_id, kickedId, OutfitMemberEventAction.Kicked()))
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
OutfitEvent(kickedBy.outfit_id, Leaving())
|
||||||
|
)
|
||||||
|
|
||||||
|
PlayerControl.sendResponse(kicked.Zone, kicked.Name,
|
||||||
|
OutfitMembershipResponse(OutfitMembershipResponse.PacketType.YouGotKicked, 0, 1,
|
||||||
|
kickedBy.CharId, kicked.CharId, kickedBy.Name, kicked.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.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
kicked.Name, AvatarAction.RemoveFromOutfitChat(kickedBy.outfit_id))
|
||||||
|
|
||||||
|
kicked.outfit_id = 0
|
||||||
|
kicked.outfit_name = ""
|
||||||
|
PlayerControl.sendResponse(kicked.Zone, kicked.Name,
|
||||||
|
OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Kicked()))
|
||||||
|
}
|
||||||
|
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.YouKicked, 0, 1, kickedBy.CharId, kickedId, name, "", flag = true))
|
||||||
|
|
||||||
|
case None => PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name,
|
||||||
|
OutfitMembershipResponse(OutfitMembershipResponse.PacketType.YouKicked, 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.Kicked()))
|
||||||
|
)
|
||||||
|
// 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.Update(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.Update(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, Initial(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.Update(
|
||||||
|
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,
|
||||||
|
Update(
|
||||||
|
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)
|
||||||
|
.filter(_.outfit_window_open)
|
||||||
|
.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 HandleOutfitRank(zones: Seq[Zone], list: List[Option[String]], player: Player): Unit = {
|
||||||
|
|
||||||
|
val outfit_id = player.outfit_id
|
||||||
|
|
||||||
|
val outfitDetails = for {
|
||||||
|
_ <- updateOutfitRanks(outfit_id, list)
|
||||||
|
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 with window open
|
||||||
|
val outfit_event = OutfitEvent(
|
||||||
|
outfit_id,
|
||||||
|
Update(
|
||||||
|
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)
|
||||||
|
.filter(_.outfit_window_open)
|
||||||
|
.foreach(player => {
|
||||||
|
PlayerControl.sendResponse(
|
||||||
|
zone, player.Name,
|
||||||
|
outfit_event
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var outfitPointSheduleStarted = false
|
||||||
|
|
||||||
|
def HandleLoginOutfitCheck(player: Player, session: SessionData): Unit = {
|
||||||
|
|
||||||
|
// TODO: implement this the proper way, please
|
||||||
|
// start the shedule on first run of the function
|
||||||
|
if (!outfitPointSheduleStarted) {
|
||||||
|
outfitPointSheduleStarted = true
|
||||||
|
|
||||||
|
Executors.newSingleThreadScheduledExecutor.scheduleAtFixedRate(
|
||||||
|
() => {
|
||||||
|
ctx.run(updateOutfitPointMV())
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Update(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[io.getquill.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[io.getquill.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[io.getquill.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 ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateOutfitRanksById(outfit_id: Long, list: List[Option[String]]): Quoted[io.getquill.Update[Outfit]] = {
|
||||||
|
|
||||||
|
// Normalize: turn empty strings into None
|
||||||
|
val normalized = list.map {
|
||||||
|
case Some(value) if value.trim.nonEmpty => Some(value)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colorize: prepend \ in front of # if it is the fist character
|
||||||
|
val colorized = normalized.map {
|
||||||
|
case Some(s) if s.startsWith("#") => Some("\\" + s)
|
||||||
|
case other => other
|
||||||
|
}
|
||||||
|
|
||||||
|
query[Outfit]
|
||||||
|
.filter(_.id == lift(outfit_id))
|
||||||
|
.update(
|
||||||
|
_.rank0 -> lift(colorized(0)),
|
||||||
|
_.rank1 -> lift(colorized(1)),
|
||||||
|
_.rank2 -> lift(colorized(2)),
|
||||||
|
_.rank3 -> lift(colorized(3)),
|
||||||
|
_.rank4 -> lift(colorized(4)),
|
||||||
|
_.rank5 -> lift(colorized(5)),
|
||||||
|
_.rank6 -> lift(colorized(6)),
|
||||||
|
_.rank7 -> lift(colorized(7))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateOutfitRanks(outfit_id: Long, list: List[Option[String]]): Future[Unit] = {
|
||||||
|
ctx.transaction { _ =>
|
||||||
|
for {
|
||||||
|
_ <- ctx.run(updateOutfitRanksById(outfit_id, list))
|
||||||
|
} yield ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateOutfitPointMV(): Quoted[Action[Unit]] = quote(
|
||||||
|
infix"REFRESH MATERIALIZED VIEW CONCURRENTLY outfitpoint_mv".as[Action[Unit]]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2532,6 +2532,9 @@ class ZoningOperations(
|
||||||
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
|
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (player.outfit_id == 0) {
|
||||||
|
SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic)
|
||||||
|
}
|
||||||
//make weather happen
|
//make weather happen
|
||||||
sendResponse(WeatherMessage(List(),List(
|
sendResponse(WeatherMessage(List(),List(
|
||||||
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
|
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
|
||||||
|
|
@ -2655,6 +2658,7 @@ class ZoningOperations(
|
||||||
log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data")
|
log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data")
|
||||||
}
|
}
|
||||||
setupAvatarFunc = AvatarCreate
|
setupAvatarFunc = AvatarCreate
|
||||||
|
SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic)
|
||||||
//make weather happen
|
//make weather happen
|
||||||
sendResponse(WeatherMessage(List(),List(
|
sendResponse(WeatherMessage(List(),List(
|
||||||
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
|
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
|
||||||
|
|
@ -3196,8 +3200,6 @@ class ZoningOperations(
|
||||||
continent.AllPlayers.filter(_.ExoSuit == ExoSuitType.MAX).foreach(max => sendResponse(PlanetsideAttributeMessage(max.GUID, 4, max.Armor)))
|
continent.AllPlayers.filter(_.ExoSuit == ExoSuitType.MAX).foreach(max => sendResponse(PlanetsideAttributeMessage(max.GUID, 4, max.Armor)))
|
||||||
// AvatarAwardMessage
|
// AvatarAwardMessage
|
||||||
//populateAvatarAwardRibbonsFunc(1, 20L)
|
//populateAvatarAwardRibbonsFunc(1, 20L)
|
||||||
|
|
||||||
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
|
|
||||||
//squad stuff (loadouts, assignment)
|
//squad stuff (loadouts, assignment)
|
||||||
sessionLogic.squad.squadSetup()
|
sessionLogic.squad.squadSetup()
|
||||||
//MapObjectStateBlockMessage and ObjectCreateMessage?
|
//MapObjectStateBlockMessage and ObjectCreateMessage?
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ class Player(var avatar: Avatar)
|
||||||
var silenced: Boolean = false
|
var silenced: Boolean = false
|
||||||
var death_by: Int = 0
|
var death_by: Int = 0
|
||||||
var lastShotSeq_time: Int = -1
|
var lastShotSeq_time: Int = -1
|
||||||
|
var outfit_name: String = ""
|
||||||
|
var outfit_id: Long = 0
|
||||||
|
var outfit_window_open: Boolean = false
|
||||||
|
var outfit_list_open: Boolean = false
|
||||||
|
|
||||||
/** From PlanetsideAttributeMessage */
|
/** From PlanetsideAttributeMessage */
|
||||||
var PlanetsideAttribute: Array[Long] = Array.ofDim(120)
|
var PlanetsideAttribute: Array[Long] = Array.ofDim(120)
|
||||||
|
|
@ -646,6 +650,8 @@ object Player {
|
||||||
obj.silenced = player.silenced
|
obj.silenced = player.silenced
|
||||||
obj.allowInteraction = player.allowInteraction
|
obj.allowInteraction = player.allowInteraction
|
||||||
obj.avatar.scorecard.respawn()
|
obj.avatar.scorecard.respawn()
|
||||||
|
obj.outfit_name = player.outfit_name
|
||||||
|
obj.outfit_id = player.outfit_id
|
||||||
obj
|
obj
|
||||||
} else {
|
} else {
|
||||||
player
|
player
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ object AvatarConverter {
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
val ab: (Boolean, Int) => CharacterAppearanceB = CharacterAppearanceB(
|
val ab: (Boolean, Int) => CharacterAppearanceB = CharacterAppearanceB(
|
||||||
0L,
|
obj.outfit_id,
|
||||||
outfit_name = "",
|
obj.outfit_name,
|
||||||
outfit_logo = 0,
|
outfit_logo = 0,
|
||||||
unk1 = false,
|
unk1 = false,
|
||||||
obj.isBackpack,
|
obj.isBackpack,
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,8 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
|
||||||
owner.name,
|
owner.name,
|
||||||
AvatarAction.AwardBep(owner.charId, deposit, ExperienceType.Normal)
|
AvatarAction.AwardBep(owner.charId, deposit, ExperienceType.Normal)
|
||||||
)
|
)
|
||||||
|
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
owner.name, AvatarAction.ShareAntExperienceWithSquad(owner, deposit, vehicle))
|
||||||
zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
|
zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,12 +121,25 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
|
||||||
} else {
|
} else {
|
||||||
(defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
|
(defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
|
||||||
}
|
}
|
||||||
val (contributionVictor, contributionOpposing, _) = {
|
val (contributionVictor, contributionOpposing) = {
|
||||||
val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
|
val (a, b) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
|
||||||
val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction }
|
//val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction }
|
||||||
(a.values, b.values, c.values)
|
(a.values, b.values)
|
||||||
}
|
}
|
||||||
val contributionVictorSize = contributionVictor.size
|
val contributionVictorSize = contributionVictor.size
|
||||||
|
|
||||||
|
//sometimes attackingFaction and defenderFaction are the same *shrug*
|
||||||
|
val failSafeOpposingFaction =
|
||||||
|
contributionOpposing
|
||||||
|
.map { case (p, _, _) => p.Faction }
|
||||||
|
.groupBy(identity)
|
||||||
|
.view.mapValues(_.size)
|
||||||
|
.toSeq
|
||||||
|
.sortBy(-_._2)
|
||||||
|
.headOption
|
||||||
|
.map(_._1)
|
||||||
|
.getOrElse(PlanetSideEmpire.NEUTRAL)
|
||||||
|
|
||||||
if (contributionVictorSize > 0) {
|
if (contributionVictorSize > 0) {
|
||||||
//setup for ...
|
//setup for ...
|
||||||
val populationIndices = playerPopulationOverTime.indices
|
val populationIndices = playerPopulationOverTime.indices
|
||||||
|
|
@ -137,7 +150,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
|
||||||
val individualPopulationByLayer = allFactions.map { f =>
|
val individualPopulationByLayer = allFactions.map { f =>
|
||||||
(f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
|
(f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
|
||||||
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
|
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
|
||||||
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
|
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(failSafeOpposingFaction))
|
||||||
}
|
}
|
||||||
val contributionOpposingSize = contributionOpposing.size
|
val contributionOpposingSize = contributionOpposing.size
|
||||||
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
|
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
|
||||||
|
|
@ -147,7 +160,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
|
||||||
building.Definition.SOIRadius.toFloat,
|
building.Definition.SOIRadius.toFloat,
|
||||||
hackStart,
|
hackStart,
|
||||||
completionTime,
|
completionTime,
|
||||||
opposingFaction,
|
failSafeOpposingFaction,
|
||||||
contributionVictor
|
contributionVictor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -215,7 +228,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
|
||||||
}
|
}
|
||||||
val heatMapModifier = FacilityHackParticipation.heatMapComparison(
|
val heatMapModifier = FacilityHackParticipation.heatMapComparison(
|
||||||
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
|
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
|
||||||
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
|
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, failSafeOpposingFaction).values
|
||||||
)
|
)
|
||||||
heatMapModifier * populationBalanceModifier
|
heatMapModifier * populationBalanceModifier
|
||||||
}
|
}
|
||||||
|
|
@ -275,13 +288,13 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
|
||||||
Config.app.game.experience.cep.rate + competitionBonus
|
Config.app.game.experience.cep.rate + competitionBonus
|
||||||
).toLong
|
).toLong
|
||||||
//8. reward participants that are still in the zone
|
//8. reward participants that are still in the zone
|
||||||
val hackerId = hacker.CharId
|
//val hackerId = hacker.CharId
|
||||||
val contributingPlayers = contributionVictor
|
val contributingPlayers = contributionVictor
|
||||||
.filter { case (player, _, _) => player.Zone.id == building.Zone.id }
|
.filter { case (player, _, _) => player.Zone.id == building.Zone.id }
|
||||||
.map { case (player, _, _) => player }
|
.map { case (player, _, _) => player }
|
||||||
.toList
|
.toList
|
||||||
//terminal hacker (always cep)
|
//terminal hacker (always cep)
|
||||||
if (contributingPlayers.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
|
/*if (contributingPlayers.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
|
||||||
ToDatabase.reportFacilityCapture(
|
ToDatabase.reportFacilityCapture(
|
||||||
hackerId,
|
hackerId,
|
||||||
zoneNumber,
|
zoneNumber,
|
||||||
|
|
@ -290,10 +303,10 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
|
||||||
expType = "cep"
|
expType = "cep"
|
||||||
)
|
)
|
||||||
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
|
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
|
||||||
}
|
}*/
|
||||||
//bystanders (cep if squad leader, bep otherwise)
|
//bystanders (cep if squad leader, bep otherwise)
|
||||||
contributingPlayers
|
contributingPlayers
|
||||||
.filterNot { _.CharId == hackerId }
|
//.filterNot { _.CharId == hackerId }
|
||||||
.foreach { player =>
|
.foreach { player =>
|
||||||
val charId = player.CharId
|
val charId = player.CharId
|
||||||
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
|
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017-2025 PSForever
|
||||||
package net.psforever.packet
|
package net.psforever.packet
|
||||||
|
|
||||||
import scodec.{Attempt, Codec, DecodeResult, Err}
|
import scodec.{Attempt, Codec, DecodeResult, Err}
|
||||||
|
|
@ -469,13 +469,13 @@ object GamePacketOpcode extends Enumeration {
|
||||||
case 0x8a => game.PlayerStasisMessage.decode
|
case 0x8a => game.PlayerStasisMessage.decode
|
||||||
case 0x8b => noDecoder(UnknownMessage139)
|
case 0x8b => noDecoder(UnknownMessage139)
|
||||||
case 0x8c => game.OutfitMembershipRequest.decode
|
case 0x8c => game.OutfitMembershipRequest.decode
|
||||||
case 0x8d => noDecoder(OutfitMembershipResponse)
|
case 0x8d => game.OutfitMembershipResponse.decode
|
||||||
case 0x8e => game.OutfitRequest.decode
|
case 0x8e => game.OutfitRequest.decode
|
||||||
case 0x8f => noDecoder(OutfitEvent)
|
case 0x8f => game.OutfitEvent.decode
|
||||||
|
|
||||||
// OPCODES 0x90-9f
|
// OPCODES 0x90-9f
|
||||||
case 0x90 => noDecoder(OutfitMemberEvent)
|
case 0x90 => game.OutfitMemberEvent.decode
|
||||||
case 0x91 => noDecoder(OutfitMemberUpdate)
|
case 0x91 => game.OutfitMemberUpdate.decode
|
||||||
case 0x92 => game.PlanetsideStringAttributeMessage.decode
|
case 0x92 => game.PlanetsideStringAttributeMessage.decode
|
||||||
case 0x93 => game.DataChallengeMessage.decode
|
case 0x93 => game.DataChallengeMessage.decode
|
||||||
case 0x94 => game.DataChallengeMessageResp.decode
|
case 0x94 => game.DataChallengeMessageResp.decode
|
||||||
|
|
@ -483,7 +483,7 @@ object GamePacketOpcode extends Enumeration {
|
||||||
case 0x96 => game.SimDataChallenge.decode
|
case 0x96 => game.SimDataChallenge.decode
|
||||||
case 0x97 => game.SimDataChallengeResp.decode
|
case 0x97 => game.SimDataChallengeResp.decode
|
||||||
// 0x98
|
// 0x98
|
||||||
case 0x98 => noDecoder(OutfitListEvent)
|
case 0x98 => game.OutfitListEvent.decode
|
||||||
case 0x99 => noDecoder(EmpireIncentivesMessage)
|
case 0x99 => noDecoder(EmpireIncentivesMessage)
|
||||||
case 0x9a => game.InvalidTerrainMessage.decode
|
case 0x9a => game.InvalidTerrainMessage.decode
|
||||||
case 0x9b => noDecoder(SyncMessage)
|
case 0x9b => noDecoder(SyncMessage)
|
||||||
|
|
|
||||||
292
src/main/scala/net/psforever/packet/game/OutfitEvent.scala
Normal file
292
src/main/scala/net/psforever/packet/game/OutfitEvent.scala
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
// 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial
|
||||||
|
*
|
||||||
|
* Send at the start of an OutfitWindow info dump.
|
||||||
|
*
|
||||||
|
* Not always complete, seen as an initialization after login, join or while outfit is in formation.
|
||||||
|
* @param outfit_info
|
||||||
|
*/
|
||||||
|
final case class Initial(
|
||||||
|
outfit_info: OutfitInfo
|
||||||
|
) extends OutfitEventAction(code = 0)
|
||||||
|
|
||||||
|
final case class Unk1(
|
||||||
|
) extends OutfitEventAction(code = 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update
|
||||||
|
*
|
||||||
|
* Send after changing outfit Ranks, MOTD and other situations.
|
||||||
|
* @param outfit_info
|
||||||
|
*/
|
||||||
|
final case class Update(
|
||||||
|
outfit_info: OutfitInfo,
|
||||||
|
) extends OutfitEventAction(code = 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send to players to tell them they left the outfit.
|
||||||
|
*
|
||||||
|
* Resets them to behave like they have no outfit.
|
||||||
|
* Will have them open the OutfitListWindow instead of the OutfitWindow.
|
||||||
|
*/
|
||||||
|
final case class Leaving(
|
||||||
|
) extends OutfitEventAction(code = 3)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to switch from the temporary "invalid" outfit ID used while formation to a valid ID used from that point on.
|
||||||
|
* @param new_outfit_id the new ID that represents this specific outfit in the DB
|
||||||
|
*/
|
||||||
|
final case class UpdateOutfitId(
|
||||||
|
new_outfit_id: Long,
|
||||||
|
) extends OutfitEventAction(code = 4)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to tell outfit members that the member count changed.
|
||||||
|
* Send after InviteAccept or Kick actions
|
||||||
|
* @param member_count
|
||||||
|
*/
|
||||||
|
final case class UpdateMemberCount(
|
||||||
|
member_count: 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[Initial] = (
|
||||||
|
("outfit_info" | InfoCodec)
|
||||||
|
).xmap[Initial](
|
||||||
|
{
|
||||||
|
case info =>
|
||||||
|
Initial(info)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case Initial(info) =>
|
||||||
|
info
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1())
|
||||||
|
|
||||||
|
val Unk2Codec: Codec[Update] = (
|
||||||
|
("outfit_info" | InfoCodec)
|
||||||
|
).xmap[Update](
|
||||||
|
{
|
||||||
|
case info =>
|
||||||
|
Update(info)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case Update(info) =>
|
||||||
|
info
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val Unk3Codec: Codec[Leaving] = PacketHelpers.emptyCodec(Leaving())
|
||||||
|
|
||||||
|
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 UpdateMemberCountCodec: Codec[UpdateMemberCount] = (
|
||||||
|
("" | uint32L)
|
||||||
|
).xmap[UpdateMemberCount](
|
||||||
|
{
|
||||||
|
case u1 =>
|
||||||
|
UpdateMemberCount(u1)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case UpdateMemberCount(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 UpdateMemberCount: 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 => UpdateMemberCountCodec
|
||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
160
src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala
Normal file
160
src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Update
|
||||||
|
*
|
||||||
|
* Update is used to inform outfit members about a new member.
|
||||||
|
* Gets send after an InviteAccept or Rank changes.
|
||||||
|
*
|
||||||
|
* @param member_name
|
||||||
|
* @param rank
|
||||||
|
* @param points client divides this by 100
|
||||||
|
* @param last_online seconds ago from current time, 0 if online
|
||||||
|
* @param action should always be 1, otherwise there will be actual data in padding. not implemented!
|
||||||
|
* @param padding should always be 0, 4 bits of padding // only contains data if action is 0
|
||||||
|
*/
|
||||||
|
final case class Update(
|
||||||
|
member_name: String,
|
||||||
|
rank: Int,
|
||||||
|
points: Long,
|
||||||
|
last_online: Long,
|
||||||
|
action: PacketType.Type,
|
||||||
|
padding: Int
|
||||||
|
) extends OutfitMemberEventAction(code = 0)
|
||||||
|
|
||||||
|
final case class Kicked(
|
||||||
|
) 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 UpdateCodec: Codec[Update] = (
|
||||||
|
("member_name" | PacketHelpers.encodedWideStringAligned(6)) ::
|
||||||
|
("rank" | uint(3)) ::
|
||||||
|
("points" | uint32L) ::
|
||||||
|
("last_login" | uint32L) ::
|
||||||
|
("action" | OutfitMemberEventAction.PacketType.codec) ::
|
||||||
|
("padding" | uint4L)
|
||||||
|
).xmap[Update](
|
||||||
|
{
|
||||||
|
case member_name :: rank :: points :: last_login :: action :: padding :: HNil =>
|
||||||
|
Update(member_name, rank, points, last_login, action, padding)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case Update(member_name, rank, points, last_login, action, padding) =>
|
||||||
|
member_name :: rank :: points :: last_login :: action :: padding :: HNil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val KickedCodec: Codec[Kicked] = PacketHelpers.emptyCodec(Kicked())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Update: PacketType.Value = Value(0)
|
||||||
|
val Kicked: PacketType.Value = Value(1)
|
||||||
|
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 => UpdateCodec
|
||||||
|
case 1 => KickedCodec
|
||||||
|
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
|
package net.psforever.packet.game
|
||||||
|
|
||||||
import net.psforever.packet.GamePacketOpcode.Type
|
import net.psforever.packet.GamePacketOpcode.Type
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||||
import net.psforever.types.PlanetSideGUID
|
|
||||||
import scodec.{Attempt, Codec, Err}
|
import scodec.{Attempt, Codec, Err}
|
||||||
import scodec.bits.BitVector
|
import scodec.bits.BitVector
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import shapeless.{::, HNil}
|
import shapeless.{::, HNil}
|
||||||
|
|
||||||
final case class OutfitMembershipRequest(
|
final case class OutfitMembershipRequest(
|
||||||
request_type: OutfitMembershipRequest.RequestType.Type,
|
requester_id: Long,
|
||||||
avatar_guid: PlanetSideGUID,
|
action: OutfitMembershipRequestAction
|
||||||
unk1: Int,
|
|
||||||
action: OutfitAction
|
|
||||||
) extends PlanetSideGamePacket {
|
) extends PlanetSideGamePacket {
|
||||||
type Packet = OutfitMembershipRequest
|
type Packet = OutfitMembershipRequest
|
||||||
|
|
||||||
|
|
@ -22,20 +19,53 @@ final case class OutfitMembershipRequest(
|
||||||
def encode: Attempt[BitVector] = OutfitMembershipRequest.encode(this)
|
def encode: Attempt[BitVector] = OutfitMembershipRequest.encode(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class OutfitAction(val code: Int)
|
abstract class OutfitMembershipRequestAction(val code: Int)
|
||||||
object OutfitAction {
|
|
||||||
|
|
||||||
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(
|
||||||
|
target_id: Long,
|
||||||
|
target_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(
|
||||||
|
target_id: Long,
|
||||||
|
target_name: String,
|
||||||
|
) extends OutfitMembershipRequestAction(code = 5)
|
||||||
|
|
||||||
|
final case class Kick(
|
||||||
|
target_id: Long,
|
||||||
|
target_name: String,
|
||||||
|
) extends OutfitMembershipRequestAction(code = 6)
|
||||||
|
|
||||||
|
final case class SetRank(
|
||||||
|
target_id: Long,
|
||||||
|
rank: Int,
|
||||||
|
target_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
|
* The `Codec`s used to transform the input stream into the context of a specific action
|
||||||
|
|
@ -44,68 +74,121 @@ object OutfitAction {
|
||||||
object Codecs {
|
object Codecs {
|
||||||
private val everFailCondition = conditional(included = false, bool)
|
private val everFailCondition = conditional(included = false, bool)
|
||||||
|
|
||||||
val CreateOutfitCodec: Codec[CreateOutfit] =
|
val CreateCodec: Codec[Create] = (
|
||||||
(PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[CreateOutfit](
|
PacketHelpers.encodedWideStringAligned(5) ::
|
||||||
{
|
PacketHelpers.encodedWideString
|
||||||
case unk2 :: unk3 :: unk4 :: outfit_name :: HNil =>
|
).xmap[Create](
|
||||||
CreateOutfit(unk2, unk3, unk4, outfit_name)
|
{
|
||||||
},
|
case u1 :: outfit_name :: HNil =>
|
||||||
{
|
Create(u1, outfit_name)
|
||||||
case CreateOutfit(unk2, unk3, unk4, outfit_name) =>
|
},
|
||||||
unk2 :: unk3 :: unk4 :: outfit_name :: HNil
|
{
|
||||||
}
|
case Create(u1, outfit_name) =>
|
||||||
)
|
u1 :: outfit_name :: HNil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val FormOutfitCodec: Codec[FormOutfit] =
|
val FormCodec: Codec[Form] = (
|
||||||
(PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[FormOutfit](
|
PacketHelpers.encodedWideStringAligned(5) ::
|
||||||
{
|
PacketHelpers.encodedWideString
|
||||||
case unk2 :: unk3 :: unk4 :: outfit_name :: HNil =>
|
).xmap[Form](
|
||||||
FormOutfit(unk2, unk3, unk4, outfit_name)
|
{
|
||||||
},
|
case u1 :: outfit_name :: HNil =>
|
||||||
{
|
Form(u1, outfit_name)
|
||||||
case FormOutfit(unk2, unk3, unk4, outfit_name) =>
|
},
|
||||||
unk2 :: unk3 :: unk4 :: outfit_name :: HNil
|
{
|
||||||
}
|
case Form(u1, outfit_name) =>
|
||||||
)
|
u1 :: outfit_name :: HNil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val AcceptOutfitCodec: Codec[AcceptOutfitInvite] =
|
val InviteCodec: Codec[Invite] = (
|
||||||
PacketHelpers.encodedWideString.xmap[AcceptOutfitInvite](
|
uint32L ::
|
||||||
{
|
PacketHelpers.encodedWideStringAligned(5)
|
||||||
case unk2 =>
|
).xmap[Invite](
|
||||||
AcceptOutfitInvite(unk2)
|
{
|
||||||
},
|
case avatar_id :: member_name :: HNil =>
|
||||||
{
|
Invite(avatar_id, member_name)
|
||||||
case AcceptOutfitInvite(unk2) =>
|
},
|
||||||
unk2
|
{
|
||||||
}
|
case Invite(avatar_id, member_name) =>
|
||||||
)
|
avatar_id :: member_name :: HNil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val RejectOutfitCodec: Codec[RejectOutfitInvite] =
|
val AcceptInviteCodec: Codec[AcceptInvite] = (
|
||||||
PacketHelpers.encodedWideString.xmap[RejectOutfitInvite](
|
PacketHelpers.encodedWideString
|
||||||
{
|
).xmap[AcceptInvite](
|
||||||
case unk2 =>
|
{
|
||||||
RejectOutfitInvite(unk2)
|
case member_name =>
|
||||||
},
|
AcceptInvite(member_name)
|
||||||
{
|
},
|
||||||
case RejectOutfitInvite(unk2) =>
|
{
|
||||||
unk2
|
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.
|
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
|
||||||
|
*
|
||||||
* @param action the action behavior code
|
* @param action the action behavior code
|
||||||
* @return a transformation between the action code and the unknown bit data
|
* @return a transformation between the action code and the unknown bit data
|
||||||
*/
|
*/
|
||||||
|
|
@ -119,11 +202,12 @@ object OutfitAction {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The action code was completely unanticipated!
|
* The action code was completely unanticipated!
|
||||||
|
*
|
||||||
* @param action the action behavior code
|
* @param action the action behavior code
|
||||||
* @return nothing; always fail
|
* @return nothing; always fail
|
||||||
*/
|
*/
|
||||||
def failureCodec(action: Int): Codec[OutfitAction] =
|
def failureCodec(action: Int): Codec[OutfitMembershipRequestAction] =
|
||||||
everFailCondition.exmap[OutfitAction](
|
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 decoding $action")),
|
||||||
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $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 OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] {
|
||||||
|
|
||||||
object RequestType extends Enumeration {
|
object PacketType extends Enumeration {
|
||||||
type Type = Value
|
type Type = Value
|
||||||
|
|
||||||
val Create: RequestType.Value = Value(0)
|
val Create: PacketType.Value = Value(0)
|
||||||
val Form: RequestType.Value = Value(1)
|
val Form: PacketType.Value = Value(1)
|
||||||
val Unk2: RequestType.Value = Value(2)
|
val Invite: PacketType.Value = Value(2)
|
||||||
val Accept: RequestType.Value = Value(3)
|
val Accept: PacketType.Value = Value(3)
|
||||||
val Reject: RequestType.Value = Value(4)
|
val Reject: PacketType.Value = Value(4)
|
||||||
val Cancel: RequestType.Value = Value(5)
|
val Cancel: PacketType.Value = Value(5)
|
||||||
val Unk6: RequestType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
|
val Kick: PacketType.Value = Value(6)
|
||||||
val Unk7: RequestType.Value = Value(7)
|
val SetRank: PacketType.Value = Value(7)
|
||||||
|
|
||||||
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
|
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def selectFromType(code: Int): Codec[OutfitAction] = {
|
private def selectFromType(code: Int): Codec[OutfitMembershipRequestAction] = {
|
||||||
import OutfitAction.Codecs._
|
import OutfitMembershipRequestAction.Codecs._
|
||||||
import scala.annotation.switch
|
import scala.annotation.switch
|
||||||
|
|
||||||
((code: @switch) match {
|
((code: @switch) match {
|
||||||
case 0 => CreateOutfitCodec
|
case 0 => CreateCodec
|
||||||
case 1 => FormOutfitCodec // so far same as Create
|
case 1 => FormCodec // so far same as Create
|
||||||
case 2 => unknownCodec(action = code)
|
case 2 => InviteCodec
|
||||||
case 3 => AcceptOutfitCodec
|
case 3 => AcceptInviteCodec
|
||||||
case 4 => RejectOutfitCodec // so far same as Accept
|
case 4 => RejectInviteCodec
|
||||||
case 5 => CancelOutfitCodec
|
case 5 => CancelInviteCodec
|
||||||
case 6 => unknownCodec(action = code)
|
case 6 => KickCodec
|
||||||
case 7 => unknownCodec(action = code)
|
case 7 => SetRankCodec
|
||||||
// 3 bit limit
|
|
||||||
case _ => failureCodec(code)
|
case _ => failureCodec(code)
|
||||||
}).asInstanceOf[Codec[OutfitAction]]
|
}).asInstanceOf[Codec[OutfitMembershipRequestAction]]
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit val codec: Codec[OutfitMembershipRequest] = (
|
implicit val codec: Codec[OutfitMembershipRequest] = (
|
||||||
("request_type" | RequestType.codec) >>:~ { request_type =>
|
("packet_type" | PacketType.codec) >>:~ { packet_type =>
|
||||||
("avatar_guid" | PlanetSideGUID.codec) ::
|
("outfit_id" | uint32L) ::
|
||||||
("unk1" | uint16L) ::
|
("action" | selectFromType(packet_type.id))
|
||||||
("action" | selectFromType(request_type.id))
|
|
||||||
}
|
}
|
||||||
).xmap[OutfitMembershipRequest](
|
).xmap[OutfitMembershipRequest](
|
||||||
{
|
{
|
||||||
case request_type :: avatar_guid :: u1 :: action :: HNil =>
|
case _ :: outfit_id :: action :: HNil =>
|
||||||
OutfitMembershipRequest(request_type, avatar_guid, u1, action)
|
OutfitMembershipRequest(outfit_id, action)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case OutfitMembershipRequest(request_type, avatar_guid, u1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type :: avatar_guid :: u1 :: action :: HNil
|
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,
|
||||||
|
requester_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 YouGotKicked: PacketType.Value = Value(4)
|
||||||
|
val YouKicked: 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
|
package net.psforever.packet.game
|
||||||
|
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||||
|
|
@ -7,77 +7,77 @@ import scodec.bits.ByteVector
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import shapeless.{::, HNil}
|
import shapeless.{::, HNil}
|
||||||
|
|
||||||
/**
|
final case class OutfitRequest(
|
||||||
* na
|
requester_id: Long,
|
||||||
*/
|
action: OutfitRequestAction
|
||||||
abstract class OutfitRequestForm(val code: Int)
|
) extends PlanetSideGamePacket {
|
||||||
|
type Packet = OutfitRequest
|
||||||
object OutfitRequestForm {
|
def opcode = GamePacketOpcode.OutfitRequest
|
||||||
/**
|
def encode = OutfitRequest.encode(this)
|
||||||
* 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* na
|
* na
|
||||||
* @param id na
|
|
||||||
* @param info na
|
|
||||||
*/
|
*/
|
||||||
final case class OutfitRequest(id: Long, info: OutfitRequestForm)
|
abstract class OutfitRequestAction(val code: Int)
|
||||||
extends PlanetSideGamePacket {
|
|
||||||
type Packet = OrbitalStrikeWaypointMessage
|
object OutfitRequestAction {
|
||||||
def opcode = GamePacketOpcode.OutfitRequest
|
/**
|
||||||
def encode = OutfitRequest.encode(this)
|
* 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 OutfitWindowOpen(menuOpen: Boolean) extends OutfitRequestAction(code = 3)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param unk na
|
||||||
|
*/
|
||||||
|
final case class OutfitListWindowOpen(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] {
|
object OutfitRequest extends Marshallable[OutfitRequest] {
|
||||||
/**
|
/**
|
||||||
* na
|
* na
|
||||||
*/
|
*/
|
||||||
private val unk0Codec: Codec[OutfitRequestForm] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist
|
private val MotdCodec: Codec[OutfitRequestAction] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist
|
||||||
.xmap[OutfitRequestForm] (
|
.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
|
* na
|
||||||
*/
|
*/
|
||||||
private val unk1Codec: Codec[OutfitRequestForm] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestForm] (
|
private val RankCodec: Codec[OutfitRequestAction] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestAction] (
|
||||||
list => OutfitRequestForm.Unk1(list),
|
list => OutfitRequestAction.Ranks(list),
|
||||||
{
|
{
|
||||||
case OutfitRequestForm.Unk1(list) => list
|
case OutfitRequestAction.Ranks(list) => list
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -104,74 +104,86 @@ object OutfitRequest extends Marshallable[OutfitRequest] {
|
||||||
/**
|
/**
|
||||||
* na
|
* 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
|
* na
|
||||||
*/
|
*/
|
||||||
private val unk3Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] (
|
private val OutfitWindowOpenCodec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] (
|
||||||
{
|
{
|
||||||
case value :: HNil => OutfitRequestForm.Unk3(value)
|
case value :: HNil => OutfitRequestAction.OutfitWindowOpen(value)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case OutfitRequestForm.Unk3(value) => value :: HNil
|
case OutfitRequestAction.OutfitWindowOpen(value) => value :: HNil
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* na
|
* na
|
||||||
*/
|
*/
|
||||||
private val unk4Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] (
|
private val OutfitListWindowOpenCodec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] (
|
||||||
{
|
{
|
||||||
case value :: HNil => OutfitRequestForm.Unk4(value)
|
case value :: HNil => OutfitRequestAction.OutfitListWindowOpen(value)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case OutfitRequestForm.Unk4(value) => value :: HNil
|
case OutfitRequestAction.OutfitListWindowOpen(value) => value :: HNil
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* na
|
* na
|
||||||
*/
|
*/
|
||||||
private def failCodec(code: Int): Codec[OutfitRequestForm] = conditional(included = false, bool).exmap[OutfitRequestForm](
|
private def failCodec(action: Int): Codec[OutfitRequestAction] = conditional(included = false, bool).exmap[OutfitRequestAction](
|
||||||
_ => Attempt.Failure(Err(s"can not decode $code-type info - what is this thing?")),
|
_ => Attempt.Failure(Err(s"can not decode $action-type info - what is this thing?")),
|
||||||
_ => Attempt.Failure(Err(s"can not encode $code-type info - no such 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 OutfitWindowOpen: PacketType.Value = Value(3)
|
||||||
|
val OutfitListWindowOpen: 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
|
* na
|
||||||
*/
|
*/
|
||||||
private def infoCodec(code: Int): Codec[OutfitRequestForm] = {
|
private def selectFromType(code: Int): Codec[OutfitRequestAction] = {
|
||||||
code match {
|
code match {
|
||||||
case 0 => unk0Codec
|
case 0 => MotdCodec
|
||||||
case 1 => unk1Codec
|
case 1 => RankCodec
|
||||||
case 2 => unk2Codec
|
case 2 => unk2Codec
|
||||||
case 3 => unk3Codec
|
case 3 => OutfitWindowOpenCodec
|
||||||
case 4 => unk4Codec
|
case 4 => OutfitListWindowOpenCodec
|
||||||
case _ => failCodec(code)
|
case _ => failCodec(code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit val codec: Codec[OutfitRequest] = (
|
implicit val codec: Codec[OutfitRequest] = (
|
||||||
uint(bits = 3) >>:~ { code =>
|
("packet_type" | PacketType.codec) >>:~ { packet_type =>
|
||||||
("id" | uint32L) ::
|
("id" | uint32L) ::
|
||||||
("info" | infoCodec(code))
|
("action" | selectFromType(packet_type.id)).hlist
|
||||||
}
|
}
|
||||||
).xmap[OutfitRequest](
|
).xmap[OutfitRequest](
|
||||||
{
|
{
|
||||||
case _:: id:: info :: HNil =>
|
case _ :: id:: action :: HNil =>
|
||||||
OutfitRequest(id, info)
|
OutfitRequest(id, action)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case OutfitRequest(id, info) =>
|
case OutfitRequest(id, action) =>
|
||||||
info.code :: id :: info :: HNil
|
OutfitRequest.PacketType(action.code) :: id :: action :: HNil
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,7 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
|
||||||
Some(outfit_id)
|
Some(outfit_id)
|
||||||
) =>
|
) =>
|
||||||
Attempt.Successful(
|
Attempt.Successful(
|
||||||
MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(
|
MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(outfit_id) :: HNil
|
||||||
outfit_id
|
|
||||||
) :: HNil
|
|
||||||
)
|
)
|
||||||
case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) =>
|
case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) =>
|
||||||
Attempt.Successful(
|
Attempt.Successful(
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,14 @@ class AvatarService(zone: Zone) extends Actor {
|
||||||
AvatarResponse.PlanetsideAttributeSelf(attribute_type, attribute_value)
|
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(
|
case AvatarAction.PlayerState(
|
||||||
guid,
|
guid,
|
||||||
pos,
|
pos,
|
||||||
|
|
@ -467,6 +475,24 @@ class AvatarService(zone: Zone) extends Actor {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case AvatarAction.ShareAntExperienceWithSquad(owner, exp, vehicle) =>
|
||||||
|
AvatarEvents.publish(
|
||||||
|
AvatarServiceResponse(
|
||||||
|
s"/$forChannel/Avatar",
|
||||||
|
Service.defaultPlayerGUID,
|
||||||
|
AvatarResponse.ShareAntExperienceWithSquad(owner, exp, vehicle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
case AvatarAction.RemoveFromOutfitChat(outfit_id) =>
|
||||||
|
AvatarEvents.publish(
|
||||||
|
AvatarServiceResponse(
|
||||||
|
s"/$forChannel/Avatar",
|
||||||
|
Service.defaultPlayerGUID,
|
||||||
|
AvatarResponse.RemoveFromOutfitChat(outfit_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package net.psforever.services.avatar
|
package net.psforever.services.avatar
|
||||||
|
|
||||||
import net.psforever.objects.Player
|
import net.psforever.objects.{Player, Vehicle}
|
||||||
import net.psforever.objects.avatar.scoring.KDAStat
|
import net.psforever.objects.avatar.scoring.KDAStat
|
||||||
import net.psforever.objects.ballistics.Projectile
|
import net.psforever.objects.ballistics.Projectile
|
||||||
import net.psforever.objects.equipment.Equipment
|
import net.psforever.objects.equipment.Equipment
|
||||||
import net.psforever.objects.inventory.InventoryItem
|
import net.psforever.objects.inventory.InventoryItem
|
||||||
import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget
|
import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget
|
||||||
import net.psforever.objects.sourcing.SourceEntry
|
import net.psforever.objects.sourcing.{SourceEntry, UniquePlayer}
|
||||||
import net.psforever.objects.vital.interaction.DamageResult
|
import net.psforever.objects.vital.interaction.DamageResult
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.packet.PlanetSideGamePacket
|
import net.psforever.packet.PlanetSideGamePacket
|
||||||
|
|
@ -78,6 +78,8 @@ object AvatarAction {
|
||||||
extends Action
|
extends Action
|
||||||
final case class PlanetsideAttributeSelf(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long)
|
final case class PlanetsideAttributeSelf(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long)
|
||||||
extends Action
|
extends Action
|
||||||
|
final case class PlanetsideStringAttribute(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: String)
|
||||||
|
extends Action
|
||||||
final case class PlayerState(
|
final case class PlayerState(
|
||||||
player_guid: PlanetSideGUID,
|
player_guid: PlanetSideGUID,
|
||||||
pos: Vector3,
|
pos: Vector3,
|
||||||
|
|
@ -161,6 +163,8 @@ object AvatarAction {
|
||||||
final case class AwardCep(charId: Long, bep: Long) extends Action
|
final case class AwardCep(charId: Long, bep: Long) extends Action
|
||||||
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action
|
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action
|
||||||
final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Action
|
final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Action
|
||||||
|
final case class ShareAntExperienceWithSquad(owner: UniquePlayer, exp: Long, vehicle: Vehicle) extends Action
|
||||||
|
final case class RemoveFromOutfitChat(outfit_id: Long) extends Action
|
||||||
|
|
||||||
final case class TeardownConnection() extends Action
|
final case class TeardownConnection() extends Action
|
||||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package net.psforever.services.avatar
|
package net.psforever.services.avatar
|
||||||
|
|
||||||
import net.psforever.objects.Player
|
import net.psforever.objects.{Player, Vehicle}
|
||||||
import net.psforever.objects.avatar.scoring.KDAStat
|
import net.psforever.objects.avatar.scoring.KDAStat
|
||||||
import net.psforever.objects.ballistics.Projectile
|
import net.psforever.objects.ballistics.Projectile
|
||||||
import net.psforever.objects.equipment.Equipment
|
import net.psforever.objects.equipment.Equipment
|
||||||
import net.psforever.objects.inventory.InventoryItem
|
import net.psforever.objects.inventory.InventoryItem
|
||||||
import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget
|
import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget
|
||||||
import net.psforever.objects.sourcing.SourceEntry
|
import net.psforever.objects.sourcing.{SourceEntry, UniquePlayer}
|
||||||
import net.psforever.objects.vital.interaction.DamageResult
|
import net.psforever.objects.vital.interaction.DamageResult
|
||||||
import net.psforever.packet.PlanetSideGamePacket
|
import net.psforever.packet.PlanetSideGamePacket
|
||||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||||
|
|
@ -47,7 +47,7 @@ object AvatarResponse {
|
||||||
final case class EquipmentInHand(pkt: ObjectCreateMessage) extends Response
|
final case class EquipmentInHand(pkt: ObjectCreateMessage) extends Response
|
||||||
final case class GenericObjectAction(object_guid: PlanetSideGUID, action_code: Int) 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 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 LoadPlayer(pkt: ObjectCreateMessage) extends Response
|
||||||
final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response
|
final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response
|
||||||
final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) 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 PlanetsideAttribute(attribute_type: Int, attribute_value: Long) extends Response
|
||||||
final case class PlanetsideAttributeToAll(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 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(
|
final case class PlayerState(
|
||||||
pos: Vector3,
|
pos: Vector3,
|
||||||
vel: Option[Vector3],
|
vel: Option[Vector3],
|
||||||
|
|
@ -133,4 +134,6 @@ object AvatarResponse {
|
||||||
final case class AwardCep(charId: Long, bep: Long) extends Response
|
final case class AwardCep(charId: Long, bep: Long) extends Response
|
||||||
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response
|
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response
|
||||||
final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Response
|
final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Response
|
||||||
|
final case class ShareAntExperienceWithSquad(owner: UniquePlayer, exp: Long, vehicle: Vehicle) extends Response
|
||||||
|
final case class RemoveFromOutfitChat(outfit_id: Long) extends Response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,5 @@ final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel
|
||||||
case object SpectatorChannel extends ChatChannel
|
case object SpectatorChannel extends ChatChannel
|
||||||
|
|
||||||
case object CustomerServiceChannel extends ChatChannel
|
case object CustomerServiceChannel extends ChatChannel
|
||||||
|
|
||||||
|
final case class OutfitChannel(id: Long) extends ChatChannel
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,9 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
|
||||||
(channel, message.messageType) match {
|
(channel, message.messageType) match {
|
||||||
case (SquadChannel(_), CMT_SQUAD) => ()
|
case (SquadChannel(_), CMT_SQUAD) => ()
|
||||||
case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
|
case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
|
||||||
case (DefaultChannel, messageType) if messageType != CMT_SQUAD => ()
|
case (OutfitChannel(_), CMT_OUTFIT) => ()
|
||||||
case (SpectatorChannel, messageType) if messageType != CMT_SQUAD => ()
|
case (DefaultChannel, messageType) if messageType != CMT_SQUAD && messageType != CMT_OUTFIT => ()
|
||||||
|
case (SpectatorChannel, messageType) if messageType != CMT_SQUAD && messageType != CMT_OUTFIT => ()
|
||||||
case _ =>
|
case _ =>
|
||||||
log.error(s"invalid chat channel $channel for messageType ${message.messageType}")
|
log.error(s"invalid chat channel $channel for messageType ${message.messageType}")
|
||||||
return this
|
return this
|
||||||
|
|
@ -158,6 +159,9 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
|
||||||
case CMT_SQUAD =>
|
case CMT_SQUAD =>
|
||||||
subs.foreach(_.actor ! MessageResponse(session, message, channel))
|
subs.foreach(_.actor ! MessageResponse(session, message, channel))
|
||||||
|
|
||||||
|
case CMT_OUTFIT =>
|
||||||
|
subs.foreach(_.actor ! MessageResponse(session, message, channel))
|
||||||
|
|
||||||
case CMT_NOTE =>
|
case CMT_NOTE =>
|
||||||
subs
|
subs
|
||||||
.filter(_.sessionSource.session.player.Name == message.recipient)
|
.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 Initial(
|
||||||
|
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,
|
||||||
|
Initial(
|
||||||
|
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 Update(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,
|
||||||
|
Update(
|
||||||
|
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 Leaving()
|
||||||
|
case _ =>
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode Unk3 ABC" in {
|
||||||
|
val msg = OutfitEvent(
|
||||||
|
2147418113L,
|
||||||
|
Leaving()
|
||||||
|
)
|
||||||
|
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 UpdateMemberCount(
|
||||||
|
member_count = 2,
|
||||||
|
)
|
||||||
|
case _ =>
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode Unk5 ABC" in {
|
||||||
|
val msg = OutfitEvent(
|
||||||
|
2147418113L,
|
||||||
|
UpdateMemberCount(
|
||||||
|
member_count = 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, Update(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,
|
||||||
|
Update(
|
||||||
|
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, Update(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,
|
||||||
|
Update(
|
||||||
|
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, Kicked()) =>
|
||||||
|
outfit_id mustEqual 529744
|
||||||
|
member_id mustEqual 41605263
|
||||||
|
case _ =>
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode Unk1" in {
|
||||||
|
val msg = OutfitMemberEvent(
|
||||||
|
outfit_id = 529744,
|
||||||
|
member_id = 41605263,
|
||||||
|
Kicked()
|
||||||
|
)
|
||||||
|
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
|
package game
|
||||||
|
|
||||||
import net.psforever.packet._
|
import net.psforever.packet._
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game.OutfitMembershipRequest
|
||||||
import net.psforever.packet.game.OutfitAction.{AcceptOutfitInvite, CancelOutfitInvite, CreateOutfit, FormOutfit, RejectOutfitInvite}
|
import net.psforever.packet.game.OutfitMembershipRequestAction._
|
||||||
import net.psforever.packet.game.OutfitMembershipRequest.RequestType
|
|
||||||
import net.psforever.types.PlanetSideGUID
|
|
||||||
import org.specs2.mutable._
|
import org.specs2.mutable._
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
class OutfitMembershipRequestTest extends Specification {
|
class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
val create_ABC = hex"8c 0 0200 000 1000 83 410042004300"
|
val create_ABC = hex"8c 0 0200 000 1000 83 410042004300"
|
||||||
val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200"
|
val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200"
|
||||||
val form_abc = hex"8c 2 0200 000 1000 83 610062006300"
|
val form_abc = hex"8c 2 0200 000 1000 83 610062006300"
|
||||||
val form_1 = hex"8c 2 1000 000 1000 81 3100"
|
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_1 = hex"8c 6 0200 000 1000"
|
||||||
val accept_2 = hex"8c 6 0400 000 1000"
|
val accept_2 = hex"8c 6 0400 000 1000"
|
||||||
val reject_1 = hex"8c 8 0200 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_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
|
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 {
|
"decode CreateOutfit ABC" in {
|
||||||
PacketCoding.decodePacket(create_ABC).require match {
|
PacketCoding.decodePacket(create_ABC).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Create
|
|
||||||
avatar_id mustEqual PlanetSideGUID(1)
|
outfit_id mustEqual 1
|
||||||
unk1 mustEqual 0
|
action mustEqual Create("", "ABC")
|
||||||
action mustEqual CreateOutfit("", 0, unk4 = false, "ABC")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode CreateOutfit ABC" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual create_ABC
|
pkt mustEqual create_ABC
|
||||||
|
|
@ -44,18 +48,16 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode CreateOutfit 2222" in {
|
"decode CreateOutfit 2222" in {
|
||||||
PacketCoding.decodePacket(create_2222).require match {
|
PacketCoding.decodePacket(create_2222).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Create
|
outfit_id mustEqual 8
|
||||||
avatar_id mustEqual PlanetSideGUID(8)
|
action mustEqual Create("", "2222")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual CreateOutfit("", 0, unk4 = false, "2222")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode CreateOutfit 2222" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual create_2222
|
pkt mustEqual create_2222
|
||||||
|
|
@ -63,18 +65,16 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode FormOutfit abc" in {
|
"decode FormOutfit abc" in {
|
||||||
PacketCoding.decodePacket(form_abc).require match {
|
PacketCoding.decodePacket(form_abc).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Form
|
outfit_id mustEqual 1
|
||||||
avatar_id mustEqual PlanetSideGUID(1)
|
action mustEqual Form("", "abc")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual FormOutfit("", 0, unk4 = false, "abc")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode FormOutfit abc" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual form_abc
|
pkt mustEqual form_abc
|
||||||
|
|
@ -82,37 +82,50 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode FormOutfit 1" in {
|
"decode FormOutfit 1" in {
|
||||||
PacketCoding.decodePacket(form_1).require match {
|
PacketCoding.decodePacket(form_1).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Form
|
outfit_id mustEqual 8
|
||||||
avatar_id mustEqual PlanetSideGUID(8)
|
action mustEqual Form("", "1")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual FormOutfit("", 0, unk4 = false, "1")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode FormOutfit 1" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual form_1
|
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 {
|
"decode AcceptOutfitInvite 1" in {
|
||||||
PacketCoding.decodePacket(accept_1).require match {
|
PacketCoding.decodePacket(accept_1).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Accept
|
outfit_id mustEqual 1
|
||||||
avatar_id mustEqual PlanetSideGUID(1)
|
action mustEqual AcceptInvite("")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual AcceptOutfitInvite("")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode AcceptOutfitInvite 1" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual accept_1
|
pkt mustEqual accept_1
|
||||||
|
|
@ -120,18 +133,16 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode AcceptOutfitInvite 2" in {
|
"decode AcceptOutfitInvite 2" in {
|
||||||
PacketCoding.decodePacket(accept_2).require match {
|
PacketCoding.decodePacket(accept_2).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Accept
|
outfit_id mustEqual 2
|
||||||
avatar_id mustEqual PlanetSideGUID(2)
|
action mustEqual AcceptInvite("")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual AcceptOutfitInvite("")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode AcceptOutfitInvite 2" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual accept_2
|
pkt mustEqual accept_2
|
||||||
|
|
@ -139,18 +150,16 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode RejectOutfitInvite 1" in {
|
"decode RejectOutfitInvite 1" in {
|
||||||
PacketCoding.decodePacket(reject_1).require match {
|
PacketCoding.decodePacket(reject_1).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Reject
|
outfit_id mustEqual 1
|
||||||
avatar_id mustEqual PlanetSideGUID(1)
|
action mustEqual RejectInvite("")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual RejectOutfitInvite("")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode RejectOutfitInvite 1" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual reject_1
|
pkt mustEqual reject_1
|
||||||
|
|
@ -158,18 +167,16 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode RejectOutfitInvite 2" in {
|
"decode RejectOutfitInvite 2" in {
|
||||||
PacketCoding.decodePacket(reject_2).require match {
|
PacketCoding.decodePacket(reject_2).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Reject
|
outfit_id mustEqual 2
|
||||||
avatar_id mustEqual PlanetSideGUID(2)
|
action mustEqual RejectInvite("")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual RejectOutfitInvite("")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode RejectOutfitInvite 2" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual reject_2
|
pkt mustEqual reject_2
|
||||||
|
|
@ -177,18 +184,16 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode CancelOutfitInvite 3" in {
|
"decode CancelOutfitInvite 3" in {
|
||||||
PacketCoding.decodePacket(cancel_3).require match {
|
PacketCoding.decodePacket(cancel_3).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Cancel
|
outfit_id mustEqual 3
|
||||||
avatar_id mustEqual PlanetSideGUID(3)
|
action mustEqual CancelInvite(0, "")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual CancelOutfitInvite(0, 0, "")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode CancelOutfitInvite 3" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual cancel_3
|
pkt mustEqual cancel_3
|
||||||
|
|
@ -196,18 +201,16 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode CancelOutfitInvite 1 abc" in {
|
"decode CancelOutfitInvite 1 abc" in {
|
||||||
PacketCoding.decodePacket(cancel_1_abc).require match {
|
PacketCoding.decodePacket(cancel_1_abc).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Cancel
|
outfit_id mustEqual 1
|
||||||
avatar_id mustEqual PlanetSideGUID(1)
|
action mustEqual CancelInvite(0, "abc")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual CancelOutfitInvite(0, 0, "abc")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode CancelOutfitInvite 1 abc" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual cancel_1_abc
|
pkt mustEqual cancel_1_abc
|
||||||
|
|
@ -215,20 +218,77 @@ class OutfitMembershipRequestTest extends Specification {
|
||||||
|
|
||||||
"decode CancelOutfitInvite 3 def" in {
|
"decode CancelOutfitInvite 3 def" in {
|
||||||
PacketCoding.decodePacket(cancel_3_def).require match {
|
PacketCoding.decodePacket(cancel_3_def).require match {
|
||||||
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
|
case OutfitMembershipRequest(outfit_id, action) =>
|
||||||
request_type mustEqual RequestType.Cancel
|
outfit_id mustEqual 3
|
||||||
avatar_id mustEqual PlanetSideGUID(3)
|
action mustEqual CancelInvite(0, "def")
|
||||||
unk1 mustEqual 0
|
|
||||||
action mustEqual CancelOutfitInvite(0, 0, "def")
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode CancelOutfitInvite 3 def" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual cancel_3_def
|
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.YouGotKicked
|
||||||
|
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.YouGotKicked, 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.YouKicked
|
||||||
|
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.YouKicked, 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.OutfitWindowOpen(value)) =>
|
||||||
|
id mustEqual 1176612L
|
||||||
|
value mustEqual true
|
||||||
|
case _ =>
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"decode Unk4" in {
|
||||||
|
PacketCoding.decodePacket(string8).require match {
|
||||||
|
case OutfitRequest(id, OutfitRequestAction.OutfitListWindowOpen(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.OutfitWindowOpen(true))
|
||||||
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
|
pkt mustEqual string6
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode Unk4" in {
|
||||||
|
val msg = OutfitRequest(41588237L, OutfitRequestAction.OutfitListWindowOpen(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
|
package game
|
||||||
|
|
||||||
import net.psforever.packet._
|
import net.psforever.packet._
|
||||||
|
|
@ -11,14 +11,14 @@ class SquadMemberEventTest extends Specification {
|
||||||
|
|
||||||
"decode" in {
|
"decode" in {
|
||||||
PacketCoding.decodePacket(string).require match {
|
PacketCoding.decodePacket(string).require match {
|
||||||
case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) =>
|
case SquadMemberEvent(event, u2, char_id, position, player_name, zone_number, outfit_id) =>
|
||||||
u1 mustEqual MemberEvent.Add
|
event mustEqual MemberEvent.Add
|
||||||
u2 mustEqual 7
|
u2 mustEqual 7
|
||||||
u3 mustEqual 42771010L
|
char_id mustEqual 42771010L
|
||||||
u4 mustEqual 0
|
position mustEqual 0
|
||||||
u5.contains("HofD") mustEqual true
|
player_name.contains("HofD") mustEqual true
|
||||||
u6.contains(7) mustEqual true
|
zone_number.contains(7) mustEqual true
|
||||||
u7.contains(529745L) mustEqual true
|
outfit_id.contains(529745L) mustEqual true
|
||||||
case _ =>
|
case _ =>
|
||||||
ko
|
ko
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue