From 912d9a6599013f04aaf6340c65fa67af36c05682 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 29 Jul 2021 09:06:29 -0400 Subject: [PATCH] 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 --- .../actors/session/SessionActor.scala | 75 ++- .../psforever/objects/GlobalDefinitions.scala | 39 +- .../objects/avatar/PlayerControl.scala | 4 +- .../objects/equipment/EffectTarget.scala | 17 +- .../EquipmentTerminalDefinition.scala | 5 + .../objects/teamwork/SquadFeatures.scala | 8 +- .../psforever/objects/vehicles/Utility.scala | 7 +- .../ProjectileDamageModifierFunctions.scala | 23 + .../net/psforever/objects/zones/Zone.scala | 42 +- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../game/LongRangeProjectileInfoMessage.scala | 29 + .../packet/game/SquadWaypointEvent.scala | 12 +- .../packet/game/SquadWaypointRequest.scala | 12 +- .../services/teamwork/SquadService.scala | 518 ++++++++++++------ .../teamwork/SquadServiceMessage.scala | 4 +- .../teamwork/SquadServiceResponse.scala | 6 +- .../services/vehicle/VehicleService.scala | 49 +- .../net/psforever/types/SquadWaypoints.scala | 77 ++- .../scala/net/psforever/zones/Zones.scala | 10 + .../LongRangeProjectileInfoMessageTest.scala | 33 ++ .../scala/game/SquadWaypointEventTest.scala | 18 +- 21 files changed, 742 insertions(+), 248 deletions(-) create mode 100644 src/main/scala/net/psforever/packet/game/LongRangeProjectileInfoMessage.scala create mode 100644 src/test/scala/game/LongRangeProjectileInfoMessageTest.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 67d57ff8..fc982b79 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -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") diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 0f5f8840..c67beb24 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 7d75e828..24f420cd 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala index 779e46c9..509134a9 100644 --- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala +++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala @@ -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 => diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala index fd5cb653..1a369a65 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala @@ -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. * diff --git a/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala b/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala index 4d2f9c29..21dbc1f9 100644 --- a/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala +++ b/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala @@ -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.
+ * 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.
*
* 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 } diff --git a/src/main/scala/net/psforever/objects/vehicles/Utility.scala b/src/main/scala/net/psforever/objects/vehicles/Utility.scala index 3aaaef1e..80e1b77b 100644 --- a/src/main/scala/net/psforever/objects/vehicles/Utility.scala +++ b/src/main/scala/net/psforever/objects/vehicles/Utility.scala @@ -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 => diff --git a/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala b/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala index d8411ea9..431f9a49 100644 --- a/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala +++ b/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala @@ -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 { /** diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 669abbc4..14a62af1 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -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 } /** diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index d12593cd..97ec7a42 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -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) diff --git a/src/main/scala/net/psforever/packet/game/LongRangeProjectileInfoMessage.scala b/src/main/scala/net/psforever/packet/game/LongRangeProjectileInfoMessage.scala new file mode 100644 index 00000000..f4455f1d --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/LongRangeProjectileInfoMessage.scala @@ -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] +} diff --git a/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala b/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala index d30bd7d6..2aabc922 100644 --- a/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala +++ b/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala @@ -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)) } diff --git a/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala b/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala index 2f42068a..77637cae 100644 --- a/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala +++ b/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala @@ -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)) } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index bcaa798d..b4db3f5e 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -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.
* 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,7 +3711,10 @@ 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 diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala index adbd12f5..9b0f8d81 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala @@ -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 diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala index fd6598fc..24861139 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala @@ -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 diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala index 0e5269bd..4c9e49b1 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala @@ -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 => diff --git a/src/main/scala/net/psforever/types/SquadWaypoints.scala b/src/main/scala/net/psforever/types/SquadWaypoints.scala index 287b25f6..dc99c5d2 100644 --- a/src/main/scala/net/psforever/types/SquadWaypoints.scala +++ b/src/main/scala/net/psforever/types/SquadWaypoints.scala @@ -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 ) } diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 7134cd53..3e9a4169 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -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 _ => ; } diff --git a/src/test/scala/game/LongRangeProjectileInfoMessageTest.scala b/src/test/scala/game/LongRangeProjectileInfoMessageTest.scala new file mode 100644 index 00000000..250f6db1 --- /dev/null +++ b/src/test/scala/game/LongRangeProjectileInfoMessageTest.scala @@ -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 + } +} diff --git a/src/test/scala/game/SquadWaypointEventTest.scala b/src/test/scala/game/SquadWaypointEventTest.scala index 4ad97d82..c855f6f3 100644 --- a/src/test/scala/game/SquadWaypointEventTest.scala +++ b/src/test/scala/game/SquadWaypointEventTest.scala @@ -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