The Flail (#896)

* corrected flail deploy animation timing; added a working laze pointer terminal utility to the flail

* initial LongRangeProjectileInfoMessage packet and tests

* flail damages targets over a distance and damage dealt will increase with distance traveled

* flail laze pointer broadcasts a special waypoint to squad members and blanks position marker after a short time

* recharge terminal will remotely restore ammunition to ancient vehicle weaponry (like the flail) as weaponfire expends it

* laze waypoints do not double and are visible to all squad members; excessive squads do not form and stick around by accident
This commit is contained in:
Fate-JH 2021-07-29 09:06:29 -04:00 committed by GitHub
parent 09e0ae3498
commit 912d9a6599
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 742 additions and 248 deletions

View file

@ -274,6 +274,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
var keepAliveFunc: () => Unit = KeepAlivePersistenceInitial
var setAvatar: Boolean = false
var turnCounterFunc: PlanetSideGUID => Unit = TurnCounterDuringInterim
var waypointCooldown: Long = 0L
var clientKeepAlive: Cancellable = Default.Cancellable
var progressBarUpdate: Cancellable = Default.Cancellable
@ -1746,7 +1747,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
}) match {
case (_, Some(adversarial)) => adversarial.attacker.Name
case (Some(reason), None) => s"a ${reason.interaction.cause.getClass.getSimpleName}"
case _ => "an unfortunate circumstance"
case _ => s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)"
}
log.info(s"${player.Name} has died, killed by $cause")
val respawnTimer = 300.seconds
@ -3991,6 +3992,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found")
}
case msg @ LongRangeProjectileInfoMessage(guid, _, _) =>
//log.info(s"$msg")
FindContainedWeapon match {
case (Some(vehicle: Vehicle), Some(weapon: Tool))
if weapon.GUID == guid => ; //now what?
case _ => ;
}
case msg @ ReleaseAvatarRequestMessage() =>
log.info(s"${player.Name} on ${continent.id} has released")
reviveTimer.cancel()
@ -4763,6 +4772,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
player,
ItemTransactionMessage(object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0))
)
} else if (tdef == GlobalDefinitions.targeting_laser_dispenser) {
//explicit request
log.info(s"${player.Name} is purchasing a targeting laser")
CancelZoningProcessWithDescriptiveReason("cancel_use")
terminal.Actor ! Terminal.Request(
player,
ItemTransactionMessage(object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0))
)
} else {
log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}")
CancelZoningProcessWithDescriptiveReason("cancel_use")
@ -4908,10 +4925,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
continent.GUID(object_guid) match {
case Some(obj: Terminal with ProximityUnit) =>
HandleProximityTerminalUse(obj)
case Some(obj) => ;
case Some(obj) =>
log.warn(s"ProximityTerminalUse: $obj does not have proximity effects for ${player.Name}")
case None =>
log.error(s"ProximityTerminalUse: ${player.Name} can not find an oject with guid $object_guid")
log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid $object_guid")
}
case msg @ UnuseItemMessage(player_guid, object_guid) =>
@ -5144,8 +5161,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
) =>
HandleWeaponFire(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten)
case msg @ WeaponLazeTargetPositionMessage(_, _, pos2) =>
log.info(s"${player.Name} is lazing the position ${continent.id}@(${pos2.x},${pos2.y},${pos2.z})")
case WeaponLazeTargetPositionMessage(_, _, _) => ;
//do not need to handle the progress bar animation/state on the server
//laze waypoint is requested by client upon completion (see SquadWaypointRequest)
val purpose = if (squad_supplement_id > 0) {
s" for ${player.Sex.possessive} squad (#${squad_supplement_id -1})"
} else {
" ..."
}
log.info(s"${player.Name} is lazing a position$purpose")
case msg @ ObjectDetectedMessage(guid1, guid2, unk, targets) =>
FindWeapon match {
@ -5177,20 +5201,34 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//find target(s)
(hit_info match {
case Some(hitInfo) =>
val hitPos = hitInfo.hit_pos
ValidObject(hitInfo.hitobject_guid) match {
case _ if projectile.profile == GlobalDefinitions.flail_projectile =>
val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius
val targets = Zone.findAllTargets(hitPos)(continent, player, projectile.profile)
.filter { target =>
Vector3.DistanceSquared(target.Position, hitPos) <= radius
}
targets.map { target =>
CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target)
(target, hitPos, target.Position)
}
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, hitInfo.hit_pos, target)
List((target, hitInfo.shot_origin, hitInfo.hit_pos))
CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target)
List((target, hitInfo.shot_origin, hitPos))
case None if projectile.profile.DamageProxy.getOrElse(0) > 0 =>
//server-side maelstrom grenade target selection
if (projectile.tool_def == GlobalDefinitions.maelstrom) {
val hitPos = hitInfo.hit_pos
val shotOrigin = hitInfo.shot_origin
val radius = projectile.profile.LashRadius * projectile.profile.LashRadius
val targets = continent.LivePlayers.filter { target =>
Vector3.DistanceSquared(target.Position, hitPos) <= radius
}
val targets = continent.blockMap
.sector(hitPos, projectile.profile.LashRadius)
.livePlayerList
.filter { target =>
Vector3.DistanceSquared(target.Position, hitPos) <= radius
}
//chainlash is separated from the actual damage application for convenience
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
@ -5199,9 +5237,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
ChainLashMessage(
hitPos,
projectile.profile.ObjectId,
targets.map {
_.GUID
}.toList
targets.map { _.GUID }
)
)
)
@ -5212,6 +5248,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} else {
Nil
}
case _ =>
Nil
}
@ -5494,7 +5531,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
)
case msg @ SquadWaypointRequest(request, _, wtype, unk, info) =>
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
val time = System.currentTimeMillis()
val subtype = wtype.subtype
if(subtype == WaypointSubtype.Squad) {
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
} else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) {
//guarding against duplicating laze waypoints
waypointCooldown = time
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
}
case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
log.info(s"${player.Name} would be in intense and excruciating pain right now if collision worked")

View file

@ -1046,6 +1046,8 @@ object GlobalDefinitions {
val repair_silo = new MedicalTerminalDefinition(729)
val recharge_terminal = new MedicalTerminalDefinition(724)
val mb_pad_creation = new VehicleSpawnPadDefinition(525)
val dropship_pad_doors = new VehicleSpawnPadDefinition(261)
@ -1108,6 +1110,8 @@ object GlobalDefinitions {
val generator = new GeneratorDefinition(351)
val obbasemesh = new AmenityDefinition(598) {}
val targeting_laser_dispenser = new OrderTerminalDefinition(851)
initMiscellaneous()
/*
@ -1584,6 +1588,18 @@ object GlobalDefinitions {
}
}
/**
* Using the definition for a `Vehicle` determine whether it is a "cavern Vehicle."
* @param vdef the `VehicleDefinition` of the item
* @return `true`, if it is; otherwise, `false`
*/
def isCavernVehicle(vdef: VehicleDefinition): Boolean = {
vdef match {
case `router` | `switchblade` | `flail` => true
case _ => false
}
}
/**
* Using the definition for a piece of `Equipment` determine whether it is "special."
* "Special equipment" is any non-standard `Equipment` that, while it can be obtained from a `Terminal`, has artificial prerequisites.
@ -2651,11 +2667,15 @@ object GlobalDefinitions {
flail_projectile.DamageRadius = 15f
flail_projectile.ProjectileDamageType = DamageType.Splash
flail_projectile.DegradeDelay = 1.5f
//a DegradeDelay of 1.5s equals a DistanceNoDegrade of 112.5m
flail_projectile.DegradeMultiplier = 5f
flail_projectile.InitialVelocity = 75
flail_projectile.Lifespan = 40f
ProjectileDefinition.CalculateDerivedFields(flail_projectile)
//TODO flail_projectile.Modifiers = RadialDegrade?
flail_projectile.Modifiers = List(
FlailDistanceDamageBoost,
RadialDegrade
)
flamethrower_fireball.Name = "flamethrower_fireball"
flamethrower_fireball.Damage0 = 30
@ -6578,13 +6598,14 @@ object GlobalDefinitions {
flail.Seats += 0 -> new SeatDefinition()
flail.controlledWeapons += 0 -> 1
flail.Weapons += 1 -> flail_weapon
flail.Utilities += 2 -> UtilityType.targeting_laser_dispenser
flail.MountPoints += 1 -> MountInfo(0)
flail.TrunkSize = InventoryTile.Tile1511
flail.TrunkOffset = 30
flail.TrunkLocation = Vector3(-3.75f, 0f, 0f)
flail.Deployment = true
flail.DeployTime = 2000
flail.UndeployTime = 2000
flail.DeployTime = 5500
flail.UndeployTime = 5500
flail.AutoPilotSpeeds = (14, 6)
flail.Packet = variantConverter
flail.DestroyedModel = Some(DestroyedVehicle.Flail)
@ -7672,6 +7693,11 @@ object GlobalDefinitions {
teleportpad_terminal.Damageable = false
teleportpad_terminal.Repairable = false
targeting_laser_dispenser.Name = "targeting_laser_dispenser"
targeting_laser_dispenser.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.flailTerminal)
targeting_laser_dispenser.Damageable = false
targeting_laser_dispenser.Repairable = false
medical_terminal.Name = "medical_terminal"
medical_terminal.Interval = 500
medical_terminal.HealAmount = 5
@ -7749,6 +7775,13 @@ object GlobalDefinitions {
repair_silo.Damageable = false
repair_silo.Repairable = false
recharge_terminal.Name = "recharge_terminal"
recharge_terminal.Interval = 1000
recharge_terminal.UseRadius = 20
recharge_terminal.TargetValidation += EffectTarget.Category.Vehicle -> EffectTarget.Validation.AncientVehicleWeaponRecharge
recharge_terminal.Damageable = false
recharge_terminal.Repairable = false
mb_pad_creation.Name = "mb_pad_creation"
mb_pad_creation.Damageable = false
mb_pad_creation.Repairable = false

View file

@ -918,9 +918,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//log message
cause.adversarial match {
case Some(a) =>
damageLog.info(s"DisplayDestroy: ${a.defender} was killed by ${a.attacker}")
damageLog.info(s"${a.defender.Name} was killed by ${a.attacker.Name}")
case _ =>
damageLog.info(s"DisplayDestroy: ${player.Name} killed ${player.Sex.pronounObject}self.")
damageLog.info(s"${player.Name} killed ${player.Sex.pronounObject}self")
}
// This would normally happen async as part of AvatarAction.Killed, but if it doesn't happen before deleting calling AvatarAction.ObjectDelete on the player the LLU will end up invisible to others if carried

View file

@ -42,7 +42,7 @@ object EffectTarget {
def RepairSilo(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle =>
!GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && v.History.exists(x => x.isInstanceOf[DamagingActivity] && x.time >= (System.currentTimeMillis() - 5000000000L))
!GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && v.History.exists(x => x.isInstanceOf[DamagingActivity] && x.time >= (System.currentTimeMillis() - 5000L))
case _ =>
false
}
@ -55,6 +55,21 @@ object EffectTarget {
false
}
def AncientVehicleWeaponRecharge(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle =>
GlobalDefinitions.isCavernVehicle(v.Definition) && v.Health > 0 &&
v.Weapons.values
.map { _.Equipment }
.flatMap {
case Some(weapon: Tool) => weapon.AmmoSlots
case _ => Nil
}
.exists { slot => slot.Box.Capacity < slot.Definition.Magazine }
case _ =>
false
}
def Player(target: PlanetSideGameObject): Boolean =
target match {
case p: Player =>

View file

@ -213,6 +213,11 @@ object EquipmentTerminalDefinition {
*/
val routerTerminal: Map[String, () => Equipment] = Map("router_telepad" -> MakeTelepad(router_telepad))
/**
* A single-element `Map` of the one piece of `Equipment` for the Flail.
*/
val flailTerminal: Map[String, () => Equipment] = Map("flail_targeting_laser" -> MakeSimpleItem(flail_targeting_laser))
/**
* Create a new `Tool` from provided `EquipmentDefinition` objects.
*

View file

@ -2,7 +2,7 @@
package net.psforever.objects.teamwork
import akka.actor.{ActorContext, ActorRef, Props}
import net.psforever.types.SquadWaypoints
import net.psforever.types.SquadWaypoint
import net.psforever.services.teamwork.SquadService.WaypointData
import net.psforever.services.teamwork.SquadSwitchboard
@ -29,7 +29,9 @@ class SquadFeatures(val Squad: Squad) {
/**
* Waypoint data.
* The first four slots are used for squad waypoints.
* The fifth slot is used for the squad leader experience waypoint.<br>
* The fifth slot is used for the squad leader experience waypoint.
* There is a sixth waypoint used for a target that has been indicated by the laze pointer
* but its id indication does not follow the indexes of the previous waypoints.<br>
* <br>
* All of the waypoints constantly exist as long as the squad to which they are attached exists.
* They are merely "activated" and "deactivated."
@ -75,7 +77,7 @@ class SquadFeatures(val Squad: Squad) {
def Start(implicit context: ActorContext): SquadFeatures = {
switchboard = context.actorOf(Props[SquadSwitchboard](), s"squad_${Squad.GUID.guid}_${System.currentTimeMillis}")
waypoints = Array.fill[WaypointData](SquadWaypoints.values.size)(new WaypointData())
waypoints = Array.fill[WaypointData](SquadWaypoint.values.size)(new WaypointData())
this
}

View file

@ -25,7 +25,8 @@ import net.psforever.types.{PlanetSideGUID, Vector3}
object UtilityType extends Enumeration {
type Type = Value
val ams_respawn_tube, bfr_rearm_terminal, lodestar_repair_terminal, matrix_terminalc, multivehicle_rearm_terminal,
order_terminala, order_terminalb, teleportpad_terminal, internal_router_telepad_deployable = Value
order_terminala, order_terminalb, targeting_laser_dispenser, teleportpad_terminal,
internal_router_telepad_deployable = Value
}
/**
@ -126,6 +127,8 @@ object Utility {
new TerminalUtility(GlobalDefinitions.order_terminala)
case UtilityType.order_terminalb =>
new TerminalUtility(GlobalDefinitions.order_terminalb)
case UtilityType.targeting_laser_dispenser =>
new TerminalUtility(GlobalDefinitions.targeting_laser_dispenser)
case UtilityType.teleportpad_terminal =>
new TeleportPadTerminalUtility(GlobalDefinitions.teleportpad_terminal)
case UtilityType.internal_router_telepad_deployable =>
@ -250,6 +253,8 @@ object Utility {
OrderTerminalDefinition.Setup
case UtilityType.order_terminalb =>
OrderTerminalDefinition.Setup
case UtilityType.targeting_laser_dispenser =>
OrderTerminalDefinition.Setup
case UtilityType.teleportpad_terminal =>
OrderTerminalDefinition.Setup
case UtilityType.internal_router_telepad_deployable =>

View file

@ -306,6 +306,29 @@ case object MeleeBoosted extends ProjectileDamageModifiers.Mod {
}
}
/**
* If the Flail's projectile exceeds it's distance before degrade in travel distance,
* the damage caused by the projectile increases by up to multiple times its base damage at 600m.
* It does not inflate for further beyond 600m.
*/
case object FlailDistanceDamageBoost extends ProjectileDamageModifiers.Mod {
override def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
val projectile = cause.projectile
val profile = projectile.profile
val distance = Vector3.Distance(data.hitPos.xy, projectile.shot_origin.xy)
val distanceNoDegrade = profile.DistanceNoDegrade
val distanceNoMultiplier = 600f - distanceNoDegrade
if (distance > profile.DistanceMax) {
0
} else if (distance >= distanceNoDegrade) {
damage + (damage * (profile.DegradeMultiplier - 1) *
math.min(distance - distanceNoDegrade, distanceNoMultiplier) / distanceNoMultiplier).toInt
} else {
damage
}
}
}
/* Functions */
object ProjectileDamageModifierFunctions {
/**

View file

@ -1193,7 +1193,45 @@ object Zone {
source: PlanetSideGameObject with Vitality,
damagePropertiesBySource: DamageWithPosition
): List[PlanetSideServerObject with Vitality] = {
val sourcePosition = source.Position
findAllTargets(zone, source.Position, damagePropertiesBySource).filter { target => target ne source }
}
/**
* na
* @see `DamageWithPosition`
* @see `Zone.blockMap.sector`
* @param sourcePosition a custom position that is used as the origin of the explosion;
* not necessarily related to source
* @param zone the zone in which the explosion should occur
* @param source a game entity that is treated as the origin and is excluded from results
* @param damagePropertiesBySource information about the effect/damage
* @return a list of affected entities
*/
def findAllTargets(
sourcePosition: Vector3
)
(
zone: Zone,
source: PlanetSideGameObject with Vitality,
damagePropertiesBySource: DamageWithPosition
): List[PlanetSideServerObject with Vitality] = {
findAllTargets(zone, sourcePosition, damagePropertiesBySource).filter { target => target ne source }
}
/**
* na
* @see `DamageWithPosition`
* @see `Zone.blockMap.sector`
* @param zone the zone in which the explosion should occur
* @param sourcePosition a position that is used as the origin of the explosion
* @param damagePropertiesBySource information about the effect/damage
* @return a list of affected entities
*/
def findAllTargets(
zone: Zone,
sourcePosition: Vector3,
damagePropertiesBySource: DamageWithPosition
): List[PlanetSideServerObject with Vitality] = {
val sourcePositionXY = sourcePosition.xy
val sectors = zone.blockMap.sector(sourcePositionXY, damagePropertiesBySource.DamageRadius)
//collect all targets that can be damaged
@ -1206,7 +1244,7 @@ object Zone {
//amenities
val soiTargets = sectors.amenityList.collect { case amenity: Vitality if !amenity.Destroyed => amenity }
//altogether ...
(playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets).filter { target => target ne source }
playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
}
/**

View file

@ -538,7 +538,7 @@ object GamePacketOpcode extends Enumeration {
case 0xc4 => game.QuantityDeltaUpdateMessage.decode
case 0xc5 => game.ChainLashMessage.decode
case 0xc6 => game.ZoneInfoMessage.decode
case 0xc7 => noDecoder(LongRangeProjectileInfoMessage)
case 0xc7 => game.LongRangeProjectileInfoMessage.decode
// 0xc8
case 0xc8 => game.WeaponLazeTargetPositionMessage.decode
case 0xc9 => noDecoder(ModuleLimitsMessage)

View file

@ -0,0 +1,29 @@
// Copyright (c) 2021 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scodec.Codec
import scodec.codecs._
final case class LongRangeProjectileInfoMessage(
guid: PlanetSideGUID,
pos: Vector3,
vel: Option[Vector3]
)
extends PlanetSideGamePacket {
type Packet = LongRangeProjectileInfoMessage
def opcode = GamePacketOpcode.LongRangeProjectileInfoMessage
def encode = LongRangeProjectileInfoMessage.encode(this)
}
object LongRangeProjectileInfoMessage extends Marshallable[LongRangeProjectileInfoMessage] {
def apply(guid: PlanetSideGUID, pos: Vector3, vel: Vector3): LongRangeProjectileInfoMessage =
LongRangeProjectileInfoMessage(guid, pos, Some(vel))
implicit val codec: Codec[LongRangeProjectileInfoMessage] = (
("guid" | PlanetSideGUID.codec) ::
("pos" | Vector3.codec_pos) ::
("vel" | optional(bool, Vector3.codec_vel))
).as[LongRangeProjectileInfoMessage]
}

View file

@ -2,7 +2,7 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.{SquadWaypoints, Vector3}
import net.psforever.types.{SquadWaypoint, Vector3}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
@ -13,7 +13,7 @@ final case class SquadWaypointEvent(
event_type: WaypointEventAction.Value,
unk: Int,
char_id: Long,
waypoint_type: SquadWaypoints.Value,
waypoint_type: SquadWaypoint,
unk5: Option[Long],
waypoint_info: Option[WaypointEvent]
) extends PlanetSideGamePacket {
@ -23,13 +23,13 @@ final case class SquadWaypointEvent(
}
object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] {
def Add(unk: Int, char_id: Long, waypoint_type: SquadWaypoints.Value, waypoint: WaypointEvent): SquadWaypointEvent =
def Add(unk: Int, char_id: Long, waypoint_type: SquadWaypoint, waypoint: WaypointEvent): SquadWaypointEvent =
SquadWaypointEvent(WaypointEventAction.Add, unk, char_id, waypoint_type, None, Some(waypoint))
def Unknown1(unk: Int, char_id: Long, waypoint_type: SquadWaypoints.Value, unk_a: Long): SquadWaypointEvent =
def Unknown1(unk: Int, char_id: Long, waypoint_type: SquadWaypoint, unk_a: Long): SquadWaypointEvent =
SquadWaypointEvent(WaypointEventAction.Unknown1, unk, char_id, waypoint_type, Some(unk_a), None)
def Remove(unk: Int, char_id: Long, waypoint_type: SquadWaypoints.Value): SquadWaypointEvent =
def Remove(unk: Int, char_id: Long, waypoint_type: SquadWaypoint): SquadWaypointEvent =
SquadWaypointEvent(WaypointEventAction.Remove, unk, char_id, waypoint_type, None, None)
private val waypoint_codec: Codec[WaypointEvent] = (
@ -42,7 +42,7 @@ object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] {
("event_type" | WaypointEventAction.codec) >>:~ { event_type =>
("unk" | uint16L) ::
("char_id" | uint32L) ::
("waypoint_type" | SquadWaypoints.codec) ::
("waypoint_type" | SquadWaypoint.codec) ::
("unk5" | conditional(event_type == WaypointEventAction.Unknown1, uint32L)) ::
("waypoint_info" | conditional(event_type == WaypointEventAction.Add, waypoint_codec))
}

View file

@ -2,7 +2,7 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.{SquadWaypoints, Vector3}
import net.psforever.types.{SquadWaypoint, Vector3}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
@ -42,7 +42,7 @@ final case class WaypointInfo(zone_number: Int, pos: Vector3)
final case class SquadWaypointRequest(
request_type: WaypointEventAction.Value,
char_id: Long,
waypoint_type: SquadWaypoints.Value,
waypoint_type: SquadWaypoint,
unk4: Option[Long],
waypoint_info: Option[WaypointInfo]
) extends PlanetSideGamePacket {
@ -52,13 +52,13 @@ final case class SquadWaypointRequest(
}
object SquadWaypointRequest extends Marshallable[SquadWaypointRequest] {
def Add(char_id: Long, waypoint_type: SquadWaypoints.Value, waypoint: WaypointInfo): SquadWaypointRequest =
def Add(char_id: Long, waypoint_type: SquadWaypoint, waypoint: WaypointInfo): SquadWaypointRequest =
SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint))
def Unknown1(char_id: Long, waypoint_type: SquadWaypoints.Value, unk_a: Long): SquadWaypointRequest =
def Unknown1(char_id: Long, waypoint_type: SquadWaypoint, unk_a: Long): SquadWaypointRequest =
SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(unk_a), None)
def Remove(char_id: Long, waypoint_type: SquadWaypoints.Value): SquadWaypointRequest =
def Remove(char_id: Long, waypoint_type: SquadWaypoint): SquadWaypointRequest =
SquadWaypointRequest(WaypointEventAction.Remove, char_id, waypoint_type, None, None)
private val waypoint_codec: Codec[WaypointInfo] = (
@ -76,7 +76,7 @@ object SquadWaypointRequest extends Marshallable[SquadWaypointRequest] {
implicit val codec: Codec[SquadWaypointRequest] = (
("request_type" | WaypointEventAction.codec) >>:~ { request_type =>
("char_id" | uint32L) ::
("waypoint_type" | SquadWaypoints.codec) ::
("waypoint_type" | SquadWaypoint.codec) ::
("unk4" | conditional(request_type == WaypointEventAction.Unknown1, uint32L)) ::
("waypoint" | conditional(request_type == WaypointEventAction.Add, waypoint_codec))
}

View file

@ -6,25 +6,16 @@ import net.psforever.objects.definition.converter.StatConverter
import net.psforever.objects.loadouts.SquadLoadout
import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures}
import net.psforever.objects.zones.Zone
import net.psforever.objects.{LivePlayerList, Player}
import net.psforever.packet.game.{
PlanetSideZoneID,
SquadDetail,
SquadInfo,
SquadPositionDetail,
SquadPositionEntry,
WaypointEventAction,
WaypointInfo,
SquadAction => SquadRequestAction
}
import net.psforever.objects.{Default, LivePlayerList, Player}
import net.psforever.packet.game.{PlanetSideZoneID, SquadDetail, SquadInfo, SquadPositionDetail, SquadPositionEntry, WaypointEventAction, WaypointInfo, SquadAction => SquadRequestAction}
import net.psforever.services.{GenericEventBus, Service}
import net.psforever.types._
import akka.actor.{Actor, ActorRef, Terminated}
import akka.actor.{Actor, ActorRef, Cancellable, Terminated}
import java.io.{PrintWriter, StringWriter}
import scala.concurrent.duration._
import scala.collection.concurrent.TrieMap
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
class SquadService extends Actor {
import SquadService._
@ -42,6 +33,15 @@ class SquadService extends Actor {
*/
private var squadFeatures: TrieMap[PlanetSideGUID, SquadFeatures] = new TrieMap[PlanetSideGUID, SquadFeatures]()
/**
* key - unique char id; value - the POSIX time after which it is cleared
*/
private var lazeIndices: Seq[LazeWaypointData] = Seq.empty
/**
* The periodic clearing of laze pointer waypoints.
*/
private var lazeIndexBlanking: Cancellable = Default.Cancellable
/**
* The list of squads that each of the factions see for the purposes of keeping track of changes to the list.
* These squads are considered public "listed" squads -
@ -49,11 +49,11 @@ class SquadService extends Actor {
* and may have limited interaction with their squad definition windows.<br>
* key - squad unique number; value - the squad's unique identifier number
*/
private val publishedLists: TrieMap[PlanetSideEmpire.Value, ListBuffer[PlanetSideGUID]] =
TrieMap[PlanetSideEmpire.Value, ListBuffer[PlanetSideGUID]](
PlanetSideEmpire.TR -> ListBuffer.empty,
PlanetSideEmpire.NC -> ListBuffer.empty,
PlanetSideEmpire.VS -> ListBuffer.empty
private val publishedLists: TrieMap[PlanetSideEmpire.Value, mutable.ListBuffer[PlanetSideGUID]] =
TrieMap[PlanetSideEmpire.Value, mutable.ListBuffer[PlanetSideGUID]](
PlanetSideEmpire.TR -> mutable.ListBuffer.empty,
PlanetSideEmpire.NC -> mutable.ListBuffer.empty,
PlanetSideEmpire.VS -> mutable.ListBuffer.empty
)
/**
@ -125,9 +125,9 @@ class SquadService extends Actor {
private[this] val log = org.log4s.getLogger
private def debug(msg: String): Unit = {
log.debug(msg)
}
private def info(msg: String): Unit = log.info(msg)
private def debug(msg: String): Unit = log.debug(msg)
override def postStop(): Unit = {
//invitations
@ -148,6 +148,8 @@ class SquadService extends Actor {
SquadEvents.unsubscribe(actor)
}
UserEvents.clear()
//misc
lazeIndices = Nil
}
/**
@ -445,6 +447,34 @@ class SquadService extends Actor {
log.warn(s"Unhandled action $msg from ${sender()}")
}
case SquadService.BlankLazeWaypoints() =>
lazeIndexBlanking.cancel()
val curr = System.currentTimeMillis()
val blank = lazeIndices.takeWhile { data => curr >= data.endTime }
lazeIndices = lazeIndices.drop(blank.size)
blank.foreach { data =>
GetParticipatingSquad(data.charId) match {
case Some(squad) =>
Publish(
squadFeatures(squad.GUID).ToChannel,
SquadResponse.WaypointEvent(WaypointEventAction.Remove, data.charId, SquadWaypoint(data.waypointType), None, None, 0),
Seq()
)
case None => ;
}
}
//retime
lazeIndices match {
case Nil => ;
case x :: _ =>
import scala.concurrent.ExecutionContext.Implicits.global
lazeIndexBlanking = context.system.scheduler.scheduleOnce(
math.min(0, x.endTime - curr).milliseconds,
self,
SquadService.BlankLazeWaypoints()
)
}
case msg =>
log.warn(s"Unhandled message $msg from ${sender()}")
}
@ -1230,7 +1260,7 @@ class SquadService extends Actor {
queuedInvites += promotedPlayer -> (xs ++ queuedInvites.remove(promotedPlayer).toList.flatten)
}
}
debug(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}")
info(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}")
Publish(features.ToChannel, SquadResponse.PromoteMember(squad, promotedPlayer, index, 0))
if (features.Listed) {
Publish(promotingPlayer, SquadResponse.SetListSquad(PlanetSideGUID(0)))
@ -1257,32 +1287,71 @@ class SquadService extends Actor {
}
}
def SquadActionWaypoint(tplayer: Player, waypointType: SquadWaypoints.Value, info: Option[WaypointInfo]): Unit = {
def SquadActionWaypoint(tplayer: Player, waypointType: SquadWaypoint, info: Option[WaypointInfo]): Unit = {
val playerCharId = tplayer.CharId
(GetLeadingSquad(tplayer, None) match {
case Some(squad) =>
info match {
case Some(winfo) =>
(Some(squad), AddWaypoint(squad.GUID, waypointType, winfo))
case _ =>
RemoveWaypoint(squad.GUID, waypointType)
(Some(squad), None)
}
case _ => (None, None)
(if (waypointType.subtype == WaypointSubtype.Laze) {
//laze rally can be updated by any squad member
GetParticipatingSquad(tplayer) match {
case Some(squad) =>
info match {
case Some(winfo) =>
//the laze-indicated target waypoint is not retained
val curr = System.currentTimeMillis()
val clippedLazes = {
val index = lazeIndices.indexWhere { _.charId == playerCharId }
if (index > -1) {
lazeIndices.take(index) ++ lazeIndices.drop(index + 1)
} else {
lazeIndices
}
}
if (lazeIndices.isEmpty || clippedLazes.headOption != lazeIndices.headOption) {
//reason to retime blanking
lazeIndexBlanking.cancel()
import scala.concurrent.ExecutionContext.Implicits.global
lazeIndexBlanking = lazeIndices.headOption match {
case Some(data) =>
context.system.scheduler.scheduleOnce(math.min(0, data.endTime - curr).milliseconds, self, SquadService.BlankLazeWaypoints())
case None =>
context.system.scheduler.scheduleOnce(15.seconds, self, SquadService.BlankLazeWaypoints())
}
}
lazeIndices = clippedLazes :+ LazeWaypointData(playerCharId, waypointType.value, curr + 15000)
(Some(squad), Some(WaypointData(winfo.zone_number, winfo.pos)))
case None =>
(Some(squad), None)
}
case None =>
(None, None)
}
} else {
//only the squad leader may update other squad waypoints
GetLeadingSquad(tplayer, None) match {
case Some(squad) =>
info match {
case Some(winfo) =>
(Some(squad), AddWaypoint(squad.GUID, waypointType, winfo))
case _ =>
RemoveWaypoint(squad.GUID, waypointType)
(Some(squad), None)
}
case None =>
(None, None)
}
}) match {
case (Some(squad), Some(_)) =>
//waypoint added or updated
Publish(
s"${squadFeatures(squad.GUID).ToChannel}",
squadFeatures(squad.GUID).ToChannel,
SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, waypointType, None, info, 1),
Seq(tplayer.CharId)
Seq(playerCharId)
)
case (Some(squad), None) =>
//waypoint removed
Publish(
s"${squadFeatures(squad.GUID).ToChannel}",
squadFeatures(squad.GUID).ToChannel,
SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, waypointType, None, None, 0),
Seq(tplayer.CharId)
Seq(playerCharId)
)
case msg =>
@ -1312,31 +1381,31 @@ class SquadService extends Actor {
SquadActionDefinitionDeleteSquadFavorite(tplayer, line, sendTo)
case ChangeSquadPurpose(purpose) =>
SquadActionDefinitionChangeSquadPurpose(tplayer, lSquadOpt, purpose)
SquadActionDefinitionChangeSquadPurpose(tplayer, pSquadOpt, lSquadOpt, purpose)
case ChangeSquadZone(zone_id) =>
SquadActionDefinitionChangeSquadZone(tplayer, lSquadOpt, zone_id, sendTo)
SquadActionDefinitionChangeSquadZone(tplayer, pSquadOpt, lSquadOpt, zone_id, sendTo)
case CloseSquadMemberPosition(position) =>
SquadActionDefinitionCloseSquadMemberPosition(tplayer, lSquadOpt, position)
SquadActionDefinitionCloseSquadMemberPosition(tplayer, pSquadOpt, lSquadOpt, position)
case AddSquadMemberPosition(position) =>
SquadActionDefinitionAddSquadMemberPosition(tplayer, lSquadOpt, position)
SquadActionDefinitionAddSquadMemberPosition(tplayer, pSquadOpt, lSquadOpt, position)
case ChangeSquadMemberRequirementsRole(position, role) =>
SquadActionDefinitionChangeSquadMemberRequirementsRole(tplayer, lSquadOpt, position, role)
SquadActionDefinitionChangeSquadMemberRequirementsRole(tplayer, pSquadOpt, lSquadOpt, position, role)
case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders(tplayer, lSquadOpt, position, orders)
SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders(tplayer, pSquadOpt, lSquadOpt, position, orders)
case ChangeSquadMemberRequirementsCertifications(position, certs) =>
SquadActionDefinitionChangeSquadMemberRequirementsCertifications(tplayer, lSquadOpt, position, certs)
SquadActionDefinitionChangeSquadMemberRequirementsCertifications(tplayer, pSquadOpt, lSquadOpt, position, certs)
case LocationFollowsSquadLead(state) =>
SquadActionDefinitionLocationFollowsSquadLead(tplayer, lSquadOpt, state)
SquadActionDefinitionLocationFollowsSquadLead(tplayer, pSquadOpt, lSquadOpt, state)
case AutoApproveInvitationRequests(state) =>
SquadActionDefinitionAutoApproveInvitationRequests(tplayer, lSquadOpt, state)
SquadActionDefinitionAutoApproveInvitationRequests(tplayer, pSquadOpt, lSquadOpt, state)
case FindLfsSoldiersForRole(position) =>
SquadActionDefinitionFindLfsSoldiersForRole(tplayer, zone, lSquadOpt, position)
@ -1345,7 +1414,7 @@ class SquadService extends Actor {
SquadActionDefinitionCancelFind(lSquadOpt)
case RequestListSquad() =>
SquadActionDefinitionRequestListSquad(tplayer, lSquadOpt, sendTo)
SquadActionDefinitionRequestListSquad(tplayer, pSquadOpt, lSquadOpt, sendTo)
case StopListSquad() =>
SquadActionDefinitionStopListSquad(tplayer, lSquadOpt, sendTo)
@ -1413,16 +1482,31 @@ class SquadService extends Actor {
}
}
def GetOrCreateSquadOnlyIfLeader(
player: Player,
participatingSquadOpt: Option[Squad],
leadingSquadOpt: Option[Squad]
): Option[Squad] = {
if (participatingSquadOpt.isEmpty) {
Some(StartSquad(player))
} else if (participatingSquadOpt == leadingSquadOpt) {
leadingSquadOpt
} else {
None
}
}
def SquadActionDefinitionSaveSquadFavorite(
tplayer: Player,
line: Int,
lSquadOpt: Option[Squad],
sendTo: ActorRef
): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
if (squad.Task.nonEmpty && squad.ZoneId > 0) {
tplayer.squadLoadouts.SaveLoadout(squad, squad.Task, line)
Publish(sendTo, SquadResponse.ListSquadFavorite(line, squad.Task))
lSquadOpt match {
case Some(squad) if squad.Task.nonEmpty && squad.ZoneId > 0 =>
tplayer.squadLoadouts.SaveLoadout(squad, squad.Task, line)
Publish(sendTo, SquadResponse.ListSquadFavorite(line, squad.Task))
case _ => ;
}
}
@ -1433,18 +1517,20 @@ class SquadService extends Actor {
lSquadOpt: Option[Squad],
sendTo: ActorRef
): Unit = {
if (pSquadOpt.isEmpty || pSquadOpt == lSquadOpt) {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
tplayer.squadLoadouts.LoadLoadout(line) match {
case Some(loadout: SquadLoadout) if squad.Size == 1 =>
SquadService.LoadSquadDefinition(squad, loadout)
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadService.PublishFullListing(squad))
Publish(sendTo, SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
InitSquadDetail(PlanetSideGUID(0), Seq(tplayer.CharId), squad)
UpdateSquadDetail(squad)
Publish(sendTo, SquadResponse.AssociateWithSquad(squad.GUID))
case _ =>
}
//TODO seems all wrong
pSquadOpt match {
case Some(squad) =>
tplayer.squadLoadouts.LoadLoadout(line) match {
case Some(loadout: SquadLoadout) if squad.Size == 1 =>
SquadService.LoadSquadDefinition(squad, loadout)
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadService.PublishFullListing(squad))
Publish(sendTo, SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
InitSquadDetail(PlanetSideGUID(0), Seq(tplayer.CharId), squad)
UpdateSquadDetail(squad)
Publish(sendTo, SquadResponse.AssociateWithSquad(squad.GUID))
case _ =>
}
case _ => ;
}
}
@ -1453,165 +1539,207 @@ class SquadService extends Actor {
Publish(sendTo, SquadResponse.ListSquadFavorite(line, ""))
}
def SquadActionDefinitionChangeSquadPurpose(tplayer: Player, lSquadOpt: Option[Squad], purpose: String): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
squad.Task = purpose
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose))
UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose))
def SquadActionDefinitionChangeSquadPurpose(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
purpose: String
): Unit = {
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
squad.Task = purpose
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose))
UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose))
case None => ;
}
}
def SquadActionDefinitionChangeSquadZone(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
zone_id: PlanetSideZoneID,
sendTo: ActorRef
): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
squad.ZoneId = zone_id.zoneId.toInt
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().ZoneId(zone_id))
InitialAssociation(squad)
Publish(sendTo, SquadResponse.Detail(squad.GUID, SquadService.PublishFullDetails(squad)))
UpdateSquadDetail(
squad.GUID,
squad.GUID,
Seq(squad.Leader.CharId),
SquadDetail().ZoneId(zone_id)
)
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
squad.ZoneId = zone_id.zoneId.toInt
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().ZoneId(zone_id))
InitialAssociation(squad)
Publish(sendTo, SquadResponse.Detail(squad.GUID, SquadService.PublishFullDetails(squad)))
UpdateSquadDetail(
squad.GUID,
squad.GUID,
Seq(squad.Leader.CharId),
SquadDetail().ZoneId(zone_id)
)
case None => ;
}
}
def SquadActionDefinitionCloseSquadMemberPosition(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
position: Int
): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
squad.Availability.lift(position) match {
case Some(true) if position > 0 => //do not close squad leader position; undefined behavior
squad.Availability.update(position, false)
val memberPosition = squad.Membership(position)
if (memberPosition.CharId > 0) {
LeaveSquad(memberPosition.CharId, squad)
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
squad.Availability.lift(position) match {
case Some(true) if position > 0 => //do not close squad leader position; undefined behavior
squad.Availability.update(position, false)
val memberPosition = squad.Membership(position)
if (memberPosition.CharId > 0) {
LeaveSquad(memberPosition.CharId, squad)
}
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
)
case Some(_) | None => ;
}
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
)
case Some(_) | None => ;
case None => ;
}
}
def SquadActionDefinitionAddSquadMemberPosition(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
position: Int
): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
squad.Availability.lift(position) match {
case Some(false) =>
squad.Availability.update(position, true)
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
)
case Some(true) | None => ;
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
squad.Availability.lift(position) match {
case Some(false) =>
squad.Availability.update(position, true)
UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
)
case Some(true) | None => ;
}
case None => ;
}
}
def SquadActionDefinitionChangeSquadMemberRequirementsRole(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
position: Int,
role: String
): Unit ={
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Role = role
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
)
case Some(false) | None => ;
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Role = role
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
)
case Some(false) | None => ;
}
case None => ;
}
}
def SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
position: Int,
orders: String
): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Orders = orders
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(
List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders)))
)
)
case Some(false) | None => ;
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Orders = orders
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(
List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders)))
)
)
case Some(false) | None => ;
}
case None => ;
}
}
def SquadActionDefinitionChangeSquadMemberRequirementsCertifications(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
position: Int,
certs: Set[Certification]
): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Requirements = certs
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
)
case Some(false) | None => ;
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Requirements = certs
UpdateSquadDetail(
squad.GUID,
SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
)
case Some(false) | None => ;
}
case None => ;
}
}
def SquadActionDefinitionLocationFollowsSquadLead(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
state: Boolean
): Unit = {
val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID)
features.LocationFollowsSquadLead = state
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
val features = squadFeatures(squad.GUID)
features.LocationFollowsSquadLead = state
case None => ;
}
}
def SquadActionDefinitionAutoApproveInvitationRequests(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
state: Boolean
): Unit = {
val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID)
features.AutoApproveInvitationRequests = state
if (state) {
//allowed auto-approval - resolve the requests (only)
val charId = tplayer.CharId
val (requests, others) = (invites.get(charId).toList ++ queuedInvites.get(charId).toList)
.partition({ case _: RequestRole => true })
invites.remove(charId)
queuedInvites.remove(charId)
previousInvites.remove(charId)
requests.foreach {
case request: RequestRole =>
JoinSquad(request.player, features.Squad, request.position)
case _ => ;
}
others.collect { case invite: Invitation => invite } match {
case Nil => ;
case x :: Nil =>
AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
case x :: xs =>
AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
queuedInvites += charId -> xs
}
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
val features = squadFeatures(squad.GUID)
features.AutoApproveInvitationRequests = state
if (state) {
//allowed auto-approval - resolve the requests (only)
val charId = tplayer.CharId
val (requests, others) = (invites.get(charId).toList ++ queuedInvites.get(charId).toList)
.partition({ case _: RequestRole => true })
invites.remove(charId)
queuedInvites.remove(charId)
previousInvites.remove(charId)
requests.foreach {
case request: RequestRole =>
JoinSquad(request.player, features.Squad, request.position)
case _ => ;
}
others.collect { case invite: Invitation => invite } match {
case Nil => ;
case x :: Nil =>
AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
case x :: xs =>
AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
queuedInvites += charId -> xs
}
}
case None => ;
}
}
@ -1745,24 +1873,36 @@ class SquadService extends Actor {
}
}
def SquadActionDefinitionRequestListSquad(tplayer: Player, lSquadOpt: Option[Squad], sendTo: ActorRef): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
val features = squadFeatures(squad.GUID)
if (!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) {
features.Listed = true
InitialAssociation(squad)
Publish(sendTo, SquadResponse.SetListSquad(squad.GUID))
UpdateSquadList(squad, None)
def SquadActionDefinitionRequestListSquad(
tplayer: Player,
pSquadOpt: Option[Squad],
lSquadOpt: Option[Squad],
sendTo: ActorRef
): Unit = {
GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match {
case Some(squad) =>
val features = squadFeatures(squad.GUID)
if (!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) {
features.Listed = true
InitialAssociation(squad)
Publish(sendTo, SquadResponse.SetListSquad(squad.GUID))
UpdateSquadList(squad, None)
}
case None => ;
}
}
def SquadActionDefinitionStopListSquad(tplayer: Player, lSquadOpt: Option[Squad], sendTo: ActorRef): Unit = {
val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
val features = squadFeatures(squad.GUID)
if (features.Listed) {
features.Listed = false
Publish(sendTo, SquadResponse.SetListSquad(PlanetSideGUID(0)))
UpdateSquadList(squad, None)
lSquadOpt match {
case Some(squad) =>
val features = squadFeatures(squad.GUID)
if (features.Listed) {
features.Listed = false
Publish(sendTo, SquadResponse.SetListSquad(PlanetSideGUID(0)))
UpdateSquadList(squad, None)
}
case None => ;
//how did we get into this situation?
}
}
@ -2868,7 +3008,7 @@ class SquadService extends Actor {
leadPosition.ZoneId = 1
squadFeatures += squad.GUID -> new SquadFeatures(squad).Start
memberToSquad += squad.Leader.CharId -> squad
debug(s"$name-$faction has created a new squad")
info(s"$name-$faction has created a new squad (#${squad.GUID.guid})")
squad
}
@ -2902,6 +3042,7 @@ class SquadService extends Actor {
val role = squad.Membership(position)
UserEvents.get(charId) match {
case Some(events) if squad.Leader.CharId != charId && squad.isAvailable(position, player.avatar.certifications) =>
info(s"${player.Name}-${player.Faction} joins position ${position+1} of squad #${squad.GUID.guid} - ${squad.Task}")
role.Name = player.Name
role.CharId = charId
role.Health = StatConverter.Health(player.Health, player.MaxHealth, min = 1, max = 64)
@ -3006,7 +3147,7 @@ class SquadService extends Actor {
def LeaveSquad(charId: Long, squad: Squad): Boolean = {
val membership = squad.Membership.zipWithIndex
membership.find { case (_member, _) => _member.CharId == charId } match {
case data @ Some((_, index)) if squad.Leader.CharId != charId =>
case data @ Some((us, index)) if squad.Leader.CharId != charId =>
PanicLeaveSquad(charId, squad, data)
//member leaves the squad completely (see PanicSquadLeave)
Publish(
@ -3019,6 +3160,7 @@ class SquadService extends Actor {
)
)
SquadEvents.unsubscribe(UserEvents(charId), s"/${squadFeatures(squad.GUID).ToChannel}/Squad")
info(s"${us.Name} has left squad #${squad.GUID.guid}")
true
case _ =>
false
@ -3051,6 +3193,7 @@ class SquadService extends Actor {
def PanicLeaveSquad(charId: Long, squad: Squad, entry: Option[(Member, Int)]): Boolean = {
entry match {
case Some((member, index)) =>
info(s"${member.Name}-${squad.Faction} has left squad #${squad.GUID.guid} - ${squad.Task}")
val entry = (charId, index)
//member leaves the squad completely
memberToSquad.remove(charId)
@ -3146,6 +3289,7 @@ class SquadService extends Actor {
squad.Membership.collect { case member if member.CharId > 0 && member.CharId != leader => member.CharId }
)
//the squad is being disbanded, the squad events channel is also going away; use cached character ids
info(s"Squad #${squad.GUID.guid} has been disbanded.")
Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None)))
}
@ -3170,6 +3314,7 @@ class SquadService extends Actor {
membership.foreach { charId =>
Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None)))
}
lazeIndices = lazeIndices.filterNot { data => membership.toSeq.contains(data.charId) }
}
/**
@ -3211,6 +3356,7 @@ class SquadService extends Actor {
* Despite the name, no waypoints are actually "added."
* All of the waypoints constantly exist as long as the squad to which they are attached exists.
* They are merely "activated" and "deactivated."
* No waypoint is ever remembered for the laze-indicated target.
* @see `SquadWaypointRequest`
* @see `WaypointInfo`
* @param guid the squad's unique identifier
@ -3220,12 +3366,12 @@ class SquadService extends Actor {
*/
def AddWaypoint(
guid: PlanetSideGUID,
waypointType: SquadWaypoints.Value,
waypointType: SquadWaypoint,
info: WaypointInfo
): Option[WaypointData] = {
squadFeatures.get(guid) match {
case Some(features) =>
features.Waypoints.lift(waypointType.id) match {
features.Waypoints.lift(waypointType.value) match {
case Some(point) =>
point.zone_number = info.zone_number
point.pos = info.pos
@ -3250,13 +3396,13 @@ class SquadService extends Actor {
* @param guid the squad's unique identifier
* @param waypointType the type of the waypoint
*/
def RemoveWaypoint(guid: PlanetSideGUID, waypointType: SquadWaypoints.Value): Unit = {
def RemoveWaypoint(guid: PlanetSideGUID, waypointType: SquadWaypoint): Unit = {
squadFeatures.get(guid) match {
case Some(features) =>
features.Waypoints.lift(waypointType.id) match {
features.Waypoints.lift(waypointType.value) match {
case Some(point) =>
point.pos = Vector3.z(1)
case _ =>
case None =>
log.warn(s"no squad waypoint $waypointType found")
}
case _ =>
@ -3281,7 +3427,7 @@ class SquadService extends Actor {
squad.Leader.CharId,
list.zipWithIndex.collect {
case (point, index) if point.pos != vz1 =>
(SquadWaypoints(index), WaypointInfo(point.zone_number, point.pos), 1)
(SquadWaypoint(index), WaypointInfo(point.zone_number, point.pos), 1)
}
)
)
@ -3565,6 +3711,9 @@ class SquadService extends Actor {
}
object SquadService {
private case class BlankLazeWaypoints()
private case class LazeWaypointData(charId: Long, waypointType: Int, endTime: Long)
/**
* Information necessary to display a specific map marker.
@ -3574,6 +3723,15 @@ object SquadService {
var pos: Vector3 = Vector3.z(1) //a waypoint with a non-zero z-coordinate will flag as not getting drawn
}
object WaypointData{
def apply(zone_number: Int, pos: Vector3): WaypointData = {
val data = new WaypointData()
data.zone_number = zone_number
data.pos = pos
data
}
}
/**
* The base of all objects that exist for the purpose of communicating invitation from one player to the next.
* @param char_id the inviting player's unique identifier number

View file

@ -4,7 +4,7 @@ package net.psforever.services.teamwork
import net.psforever.objects.Player
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{WaypointEventAction, WaypointInfo, SquadAction => PacketSquadAction}
import net.psforever.types.{PlanetSideGUID, SquadRequestType, SquadWaypoints, Vector3}
import net.psforever.types.{PlanetSideGUID, SquadRequestType, SquadWaypoint, Vector3}
final case class SquadServiceMessage(tplayer: Player, zone: Zone, actionMessage: Any)
@ -29,7 +29,7 @@ object SquadAction {
) extends Action
final case class Waypoint(
event_type: WaypointEventAction.Value,
waypoint_type: SquadWaypoints.Value,
waypoint_type: SquadWaypoint,
unk: Option[Long],
waypoint_info: Option[WaypointInfo]
) extends Action

View file

@ -3,7 +3,7 @@ package net.psforever.services.teamwork
import net.psforever.objects.teamwork.Squad
import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo}
import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoints}
import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoint}
import net.psforever.services.GenericEventBusMsg
final case class SquadServiceResponse(channel: String, exclude: Iterable[Long], response: SquadResponse.Response)
@ -48,12 +48,12 @@ object SquadResponse {
final case class Detail(guid: PlanetSideGUID, squad_detail: SquadDetail) extends Response
final case class InitWaypoints(char_id: Long, waypoints: Iterable[(SquadWaypoints.Value, WaypointInfo, Int)])
final case class InitWaypoints(char_id: Long, waypoints: Iterable[(SquadWaypoint, WaypointInfo, Int)])
extends Response
final case class WaypointEvent(
event_type: WaypointEventAction.Value,
char_id: Long,
waypoint_type: SquadWaypoints.Value,
waypoint_type: SquadWaypoint,
unk5: Option[Long],
waypoint_info: Option[WaypointInfo],
unk: Int

View file

@ -2,7 +2,7 @@
package net.psforever.services.vehicle
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.objects.Vehicle
import net.psforever.objects.{GlobalDefinitions, Tool, Vehicle}
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
import net.psforever.objects.serverobject.terminals.{MedicalTerminalDefinition, ProximityUnit}
@ -352,16 +352,45 @@ class VehicleService(zone: Zone) extends Actor {
case ProximityUnit.Action(term, target: Vehicle) =>
val medDef = term.Definition.asInstanceOf[MedicalTerminalDefinition]
val healAmount = medDef.HealAmount
if (healAmount != 0 && term.Validate(target) && target.Health < target.MaxHealth) {
target.Health = target.Health + healAmount
target.History(RepairFromTerm(VehicleSource(target), healAmount, medDef))
VehicleEvents.publish(
VehicleServiceResponse(
s"/${term.Continent}/Vehicle",
PlanetSideGUID(0),
VehicleResponse.PlanetsideAttribute(target.GUID, 0, target.Health)
if (!target.Destroyed && term.Validate(target)) {
//repair vehicle
if (healAmount > 0 && target.Health < target.MaxHealth) {
val healAmount = medDef.HealAmount
target.Health = target.Health + healAmount
target.History(RepairFromTerm(VehicleSource(target), healAmount, medDef))
VehicleEvents.publish(
VehicleServiceResponse(
s"/${term.Continent}/Vehicle",
PlanetSideGUID(0),
VehicleResponse.PlanetsideAttribute(target.GUID, 0, target.Health)
)
)
)
}
//recharge ammunition of cavern vehicles
if (GlobalDefinitions.isCavernVehicle(target.Definition) && term.Definition == GlobalDefinitions.recharge_terminal) {
//TODO check cavern module benefits on facility; unlike facility benefits, it's faked for now
val channel = s"/${target.Actor.toString}/Vehicle"
val parent = target.GUID
val excludeNone = Service.defaultPlayerGUID
target.Weapons.values
.map { _.Equipment }
.collect { case Some(weapon: Tool) =>
weapon.AmmoSlots
.foreach { slot =>
val box = slot.Box
if (box.Capacity < slot.Definition.Magazine) {
val capacity = box.Capacity += 1
VehicleEvents.publish(
VehicleServiceResponse(
channel,
excludeNone,
VehicleResponse.InventoryState2(box.GUID, parent, capacity)
)
)
}
}
}
}
}
case msg =>

View file

@ -1,12 +1,81 @@
// Copyright (c) 2019 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
object SquadWaypoints extends Enumeration {
/**
* Distinction of purpose of the waypoint.
*/
object WaypointSubtype extends Enumeration {
type Type = Value
val One, Two, Three, Four, ExperienceRally = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
val Squad, Laze = Value
}
/**
* Base of all waypoints visible to members of a particular squad.
*/
sealed trait SquadWaypoint {
/** the index of this kind of waypoint */
def value: Int
/** the distinction of this kind of waypoint */
def subtype: WaypointSubtype.Value
}
/**
* Permanently-defined waypoints known to all squads, set only by the squad leader, accessible by command rank status.
*/
sealed abstract class StandardWaypoint(override val value: Int) extends SquadWaypoint {
def subtype: WaypointSubtype.Value = WaypointSubtype.Squad
}
/**
* General waypoint produced by the Flail targeting laser (laze pointer)
* that is visible by all squad members for a short duration.
* Any squad member may place this waypoint.
* A laze waypoint is yellow,
* is indicated in the game world, on the proximity map, and on the continental map,
* and is designated by the number of the squad member that produced it.
* Only one laze waypoint may be made visible from any one squad member at any given time, overwritten when replaced.
* When viewed by a squad member seated in a Flail, the waypoint includes an elevation reticle for aiming purposes.
* YMMV.
* @see `SquadWaypointEvent`
* @see `SquadWaypointRequest`
* @param value the index of the waypoint can be any number five and above
*/
sealed case class LazeWaypoint(value: Int) extends SquadWaypoint {
def subtype: WaypointSubtype.Value = WaypointSubtype.Laze
}
object SquadWaypoint {
/**
* Overloaded constructor
* that returns either the specific squad waypoint as the index value
* or a laze waypoint with the same value.
* @param value the index of this kind of waypoint
* @return a waypoint object
*/
def apply(value: Int): SquadWaypoint = {
if(value < 5) {
values(value)
} else {
LazeWaypoint(value)
}
}
/** the five squad-specific waypoints */
//does not include the multitude of possible laze waypoints
def values = Seq(One, Two, Three, Four, ExperienceRally)
/** the first squad rally */
case object One extends StandardWaypoint(value = 0)
/** the second squad rally */
case object Two extends StandardWaypoint(value = 1)
/** the third squad rally */
case object Three extends StandardWaypoint(value = 2)
/** the fourth squad rally */
case object Four extends StandardWaypoint(value = 3)
/** the squad experience bonus rally */
case object ExperienceRally extends StandardWaypoint(value = 4)
implicit val codec = uint8L.xmap[SquadWaypoint]( n => apply(n), waypoint => waypoint.value )
}

View file

@ -490,6 +490,11 @@ object Zones {
Terminal.Constructor(obj.position, GlobalDefinitions.ground_rearm_terminal),
owningBuildingGuid = ownerGuid
)
zoneMap.addLocalObject(
obj.guid + 3,
ProximityTerminal.Constructor(obj.position, GlobalDefinitions.recharge_terminal),
owningBuildingGuid = ownerGuid
)
case "pad_landing_frame" | "pad_landing_tower_frame" =>
// startup.pak-out/game_objects.adb.lst:22518:add_property pad_landing_frame has_aggregate_rearm_terminal true
// startup.pak-out/game_objects.adb.lst:22519:add_property pad_landing_frame has_aggregate_recharge_terminal true
@ -500,6 +505,11 @@ object Zones {
Terminal.Constructor(obj.position, GlobalDefinitions.air_rearm_terminal),
owningBuildingGuid = ownerGuid
)
zoneMap.addLocalObject(
obj.guid + 2,
ProximityTerminal.Constructor(obj.position, GlobalDefinitions.recharge_terminal),
owningBuildingGuid = ownerGuid
)
case _ => ;
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.{PlanetSideGUID, Vector3}
import scodec.bits._
class LongRangeProjectileInfoMessageTest extends Specification {
val string = hex"c7 d214 006c485fd9c307ed30790f84a0"
"decode" in {
PacketCoding.decodePacket(string).require match {
case LongRangeProjectileInfoMessage(guid, pos, vel) =>
guid mustEqual PlanetSideGUID(5330)
pos mustEqual Vector3(2264, 5115.039f, 31.046875f)
vel.contains(Vector3(-57.1875f, 9.875f, 47.5f)) mustEqual true
case _ =>
ko
}
}
"encode" in {
val msg = LongRangeProjectileInfoMessage(
PlanetSideGUID(5330),
Vector3(2264, 5115.039f, 31.046875f),
Vector3(-57.1875f, 9.875f, 47.5f)
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -4,7 +4,7 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game.{SquadWaypointEvent, WaypointEvent, WaypointEventAction}
import net.psforever.types.{SquadWaypoints, Vector3}
import net.psforever.types.{SquadWaypoint, Vector3}
import scodec.bits._
class SquadWaypointEventTest extends Specification {
@ -19,7 +19,7 @@ class SquadWaypointEventTest extends Specification {
unk1 mustEqual WaypointEventAction.Remove
unk2 mustEqual 11
unk3 mustEqual 31155863L
unk4 mustEqual SquadWaypoints.One
unk4 mustEqual SquadWaypoint.One
unk5.isEmpty mustEqual true
unk6.isEmpty mustEqual true
case _ =>
@ -33,7 +33,7 @@ class SquadWaypointEventTest extends Specification {
unk1 mustEqual WaypointEventAction.Remove
unk2 mustEqual 10
unk3 mustEqual 0L
unk4 mustEqual SquadWaypoints.ExperienceRally
unk4 mustEqual SquadWaypoint.ExperienceRally
unk5.isEmpty mustEqual true
unk6.isEmpty mustEqual true
case _ =>
@ -47,7 +47,7 @@ class SquadWaypointEventTest extends Specification {
unk1 mustEqual WaypointEventAction.Add
unk2 mustEqual 3
unk3 mustEqual 41581052L
unk4 mustEqual SquadWaypoints.Two
unk4 mustEqual SquadWaypoint.Two
unk5.isEmpty mustEqual true
unk6.contains(WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1)) mustEqual true
case _ =>
@ -61,7 +61,7 @@ class SquadWaypointEventTest extends Specification {
unk1 mustEqual WaypointEventAction.Unknown1
unk2 mustEqual 3
unk3 mustEqual 41581052L
unk4 mustEqual SquadWaypoints.Two
unk4 mustEqual SquadWaypoint.Two
unk5.contains(4L) mustEqual true
unk6.isEmpty mustEqual true
case _ =>
@ -70,14 +70,14 @@ class SquadWaypointEventTest extends Specification {
}
"encode (1)" in {
val msg = SquadWaypointEvent.Remove(11, 31155863L, SquadWaypoints.One)
val msg = SquadWaypointEvent.Remove(11, 31155863L, SquadWaypoint.One)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_1
}
"encode (2)" in {
val msg = SquadWaypointEvent.Remove(10, 0L, SquadWaypoints.ExperienceRally)
val msg = SquadWaypointEvent.Remove(10, 0L, SquadWaypoint.ExperienceRally)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_2
@ -87,7 +87,7 @@ class SquadWaypointEventTest extends Specification {
val msg = SquadWaypointEvent.Add(
3,
41581052L,
SquadWaypoints.Two,
SquadWaypoint.Two,
WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1)
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
@ -96,7 +96,7 @@ class SquadWaypointEventTest extends Specification {
}
"encode (4)" in {
val msg = SquadWaypointEvent.Unknown1(3, 41581052L, SquadWaypoints.Two, 4L)
val msg = SquadWaypointEvent.Unknown1(3, 41581052L, SquadWaypoint.Two, 4L)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_4