diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 87f6cbd1..1c25abaa 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -373,10 +373,9 @@ object Zone { /** * Message that returns a discovered spawn point to a request source. * @param zone_id the zone's text identifier - * @param building the `Building` in which the spawnpoint is located * @param spawn_tube the spawn point holding object */ - final case class SpawnPoint(zone_id : String, building : Building, spawn_tube : SpawnTube) + final case class SpawnPoint(zone_id : String, spawn_tube : SpawnTube) /** * Message that informs a request source that a spawn point could not be discovered with the previous criteria. * @param zone_number this zone's numeric identifier diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index f14f31bb..4d6f0c4f 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -4,9 +4,11 @@ package net.psforever.objects.zones import java.util.concurrent.atomic.AtomicInteger import akka.actor.Actor -import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject} import net.psforever.objects.serverobject.structures.StructureType -import net.psforever.types.Vector3 +import net.psforever.objects.serverobject.tube.SpawnTube +import net.psforever.objects.vehicles.UtilityType +import net.psforever.types.{DriveState, Vector3} import org.log4s.Logger /** @@ -64,30 +66,61 @@ class ZoneActor(zone : Zone) extends Actor { //own case Zone.Lattice.RequestSpawnPoint(zone_number, player, spawn_group) => if(zone_number == zone.Number) { - val buildingTypeSet = if(spawn_group == 6) { - Set(StructureType.Tower) - } - else if(spawn_group == 7) { - Set(StructureType.Facility, StructureType.Building) - } - else { - Set.empty[StructureType.Value] - } val playerPosition = player.Position.xy - zone.SpawnGroups() - .filter({ case((building, _)) => - building.Faction == player.Faction && buildingTypeSet.contains(building.BuildingType) - }) - .toSeq - .sortBy({ case ((building, _)) => - Vector3.DistanceSquared(playerPosition, building.Position.xy) - }) - .headOption match { - case Some((building, List(tube))) => - sender ! Zone.Lattice.SpawnPoint(zone.Id, building, tube) + ( + if(spawn_group == 2) { + //ams + zone.Vehicles + .filter(veh => + veh.DeploymentState == DriveState.Deployed && + veh.Definition == GlobalDefinitions.ams && + veh.Faction == player.Faction + ) + .sortBy(veh => Vector3.DistanceSquared(playerPosition, veh.Position.xy)) + .flatMap(veh => veh.Utilities.values.filter(util => util.UtilType == UtilityType.ams_respawn_tube)) + .headOption match { + case None => + None + case Some(util) => + Some(List(util().asInstanceOf[SpawnTube])) + } + } + else { + //facilities, towers, and buildings + val buildingTypeSet = if(spawn_group == 0) { + Set(StructureType.Facility, StructureType.Tower, StructureType.Building) + } + else if(spawn_group == 6) { + Set(StructureType.Tower) + } + else if(spawn_group == 7) { + Set(StructureType.Facility, StructureType.Building) + } + else { + Set.empty[StructureType.Value] + } + zone.SpawnGroups() + .filter({ case ((building, _)) => + building.Faction == player.Faction && + buildingTypeSet.contains(building.BuildingType) + }) + .toSeq + .sortBy({ case ((building, _)) => + Vector3.DistanceSquared(playerPosition, building.Position.xy) + }) + .headOption match { + case None | Some((_, Nil)) => + None + case Some((_, tubes)) => + Some(tubes) + } + } + ) match { + case Some(List(tube)) => + sender ! Zone.Lattice.SpawnPoint(zone.Id, tube) - case Some((building, tubes)) => - sender ! Zone.Lattice.SpawnPoint(zone.Id, building, scala.util.Random.shuffle(tubes).head) + case Some(tubes) => + sender ! Zone.Lattice.SpawnPoint(zone.Id, scala.util.Random.shuffle(tubes).head) case None => sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, Some(spawn_group)) @@ -143,6 +176,68 @@ class ZoneActor(zone : Zone) extends Actor { } object ZoneActor { +// import net.psforever.types.PlanetSideEmpire +// import net.psforever.objects.Vehicle +// import net.psforever.objects.serverobject.structures.Building +// def AllSpawnGroup(zone : Zone, targetPosition : Vector3, targetFaction : PlanetSideEmpire.Value) : Option[List[SpawnTube]] = { +// ClosestOwnedSpawnTube(AmsSpawnGroup(zone) ++ BuildingSpawnGroup(zone, 0), targetPosition, targetFaction) +// } +// +// def AmsSpawnGroup(vehicles : List[Vehicle]) : Iterable[(Vector3, PlanetSideEmpire.Value, Iterable[SpawnTube])] = { +// vehicles +// .filter(veh => veh.DeploymentState == DriveState.Deployed && veh.Definition == GlobalDefinitions.ams) +// .map(veh => +// (veh.Position, veh.Faction, +// veh.Utilities +// .values +// .filter(util => util.UtilType == UtilityType.ams_respawn_tube) +// .map { _().asInstanceOf[SpawnTube] } +// ) +// ) +// } +// +// def AmsSpawnGroup(zone : Zone, spawn_group : Int = 2) : Iterable[(Vector3, PlanetSideEmpire.Value, Iterable[SpawnTube])] = { +// if(spawn_group == 2) { +// AmsSpawnGroup(zone.Vehicles) +// } +// else { +// Nil +// } +// } +// +// def BuildingSpawnGroup(spawnGroups : Map[Building, List[SpawnTube]]) : Iterable[(Vector3, PlanetSideEmpire.Value, Iterable[SpawnTube])] = { +// spawnGroups +// .map({ case ((building, tubes)) => (building.Position.xy, building.Faction, tubes) }) +// } +// +// def BuildingSpawnGroup(zone : Zone, spawn_group : Int) : Iterable[(Vector3, PlanetSideEmpire.Value, Iterable[SpawnTube])] = { +// val buildingTypeSet = if(spawn_group == 0) { +// Set(StructureType.Facility, StructureType.Tower, StructureType.Building) +// } +// else if(spawn_group == 6) { +// Set(StructureType.Tower) +// } +// else if(spawn_group == 7) { +// Set(StructureType.Facility, StructureType.Building) +// } +// else { +// Set.empty[StructureType.Value] +// } +// BuildingSpawnGroup( +// zone.SpawnGroups().filter({ case((building, _)) => buildingTypeSet.contains(building.BuildingType) }) +// ) +// } +// +// def ClosestOwnedSpawnTube(tubes : Iterable[(Vector3, PlanetSideEmpire.Value, Iterable[SpawnTube])], targetPosition : Vector3, targetFaction : PlanetSideEmpire.Value) : Option[List[SpawnTube]] = { +// tubes +// .toSeq +// .filter({ case (_, faction, _) => faction == targetFaction }) +// .sortBy({ case (pos, _, _) => Vector3.DistanceSquared(pos, targetPosition) }) +// .take(1) +// .map({ case (_, _, tubes : List[SpawnTube]) => tubes }) +// .headOption +// } + /** * Recover an object from a collection and perform any number of validating tests upon it. * If the object fails any tests, log an error. diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 9d586476..f1a6ea45 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -475,7 +475,7 @@ object GamePacketOpcode extends Enumeration { case 0x81 => game.DestroyDisplayMessage.decode case 0x82 => noDecoder(TriggerBotAction) case 0x83 => noDecoder(SquadWaypointRequest) - case 0x84 => noDecoder(SquadWaypointEvent) + case 0x84 => game.SquadWaypointEvent.decode case 0x85 => noDecoder(OffshoreVehicleMessage) case 0x86 => game.ObjectDeployedMessage.decode case 0x87 => noDecoder(ObjectDeployedCountMessage) @@ -568,7 +568,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0xd0-df case 0xd0 => noDecoder(UnknownMessage208) case 0xd1 => game.DisplayedAwardMessage.decode - case 0xd2 => noDecoder(RespawnAMSInfoMessage) + case 0xd2 => game.RespawnAMSInfoMessage.decode case 0xd3 => noDecoder(ComponentDamageMessage) case 0xd4 => noDecoder(GenericObjectActionAtPositionMessage) case 0xd5 => game.PropertyOverrideMessage.decode diff --git a/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala b/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala index cecd28ea..ad582144 100644 --- a/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala @@ -7,6 +7,8 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} +import scala.annotation.tailrec + /** * A `Codec` for the actions that each layer of the diagram performs. * `Style`, `Vertex`, `Action5`, `DrawString`, and `Action7` have additional `DiagramStroke` input data. @@ -152,7 +154,7 @@ final case class BattleDiagramAction(action : DiagramActionCode.Value, */ final case class BattleplanMessage(char_id : Long, player_name : String, - zone_id : PlanetSideGUID, + zone_id : Int, diagrams : List[BattleDiagramAction]) extends PlanetSideGamePacket { type Packet = BattleplanMessage @@ -365,11 +367,11 @@ object BattleplanMessage extends Marshallable[BattleplanMessage] { * @param list a `List` of extracted `BattleDiagrams`; * technically, the output */ - private def rollDiagramLayers(element : Option[BattleDiagramChain], list : ListBuffer[BattleDiagramAction]) : Unit = { - if(element.isEmpty) - return - list += element.get.diagram - rollDiagramLayers(element.get.next, list) //tail call optimization + @tailrec private def rollDiagramLayers(element : Option[BattleDiagramChain], list : ListBuffer[BattleDiagramAction]) : Unit = { + if(element.nonEmpty) { + list += element.get.diagram + rollDiagramLayers(element.get.next, list) + } } /** @@ -380,17 +382,20 @@ object BattleplanMessage extends Marshallable[BattleplanMessage] { * technically, the output * @return a linked list of `BattleDiagramChain` objects */ - private def unrollDiagramLayers(revIter : Iterator[BattleDiagramAction], layers : Option[BattleDiagramChain] = None) : Option[BattleDiagramChain] = { - if(!revIter.hasNext) - return layers - val elem : BattleDiagramAction = revIter.next - unrollDiagramLayers(revIter, Some(BattleDiagramChain(elem, layers))) //tail call optimization + @tailrec private def unrollDiagramLayers(revIter : Iterator[BattleDiagramAction], layers : Option[BattleDiagramChain] = None) : Option[BattleDiagramChain] = { + if(!revIter.hasNext) { + layers + } + else { + val elem : BattleDiagramAction = revIter.next + unrollDiagramLayers(revIter, Some(BattleDiagramChain(elem, layers))) + } } implicit val codec : Codec[BattleplanMessage] = ( ("char_id" | uint32L) :: ("player_name" | PacketHelpers.encodedWideString) :: - ("zone_id" | PlanetSideGUID.codec) :: + ("zone_id" | uint16L) :: (uint8L >>:~ { count => conditional(count > 0, "diagrams" | parse_diagrams_codec(count)).hlist }) diff --git a/common/src/main/scala/net/psforever/packet/game/RespawnAMSInfoMessage.scala b/common/src/main/scala/net/psforever/packet/game/RespawnAMSInfoMessage.scala new file mode 100644 index 00000000..d0361623 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/RespawnAMSInfoMessage.scala @@ -0,0 +1,75 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class RespawnInfo(unk1 : List[Vector3], + unk2 : List[Boolean]) + +final case class RespawnAMSInfoMessage(unk1 : PlanetSideGUID, + unk2 : Boolean, + unk3 : Option[RespawnInfo]) + extends PlanetSideGamePacket { + type Packet = RespawnAMSInfoMessage + def opcode = GamePacketOpcode.RespawnAMSInfoMessage + def encode = RespawnAMSInfoMessage.encode(this) +} + +object RespawnAMSInfoMessage extends Marshallable[RespawnAMSInfoMessage] { + def apply(u1 : PlanetSideGUID, u2 : Boolean) : RespawnAMSInfoMessage = { + RespawnAMSInfoMessage(u1, u2, None) + } + + def apply(u1 : PlanetSideGUID, u2 : Boolean, u3 : RespawnInfo) : RespawnAMSInfoMessage = { + RespawnAMSInfoMessage(u1, u2, Some(u3)) + } + + private val info_codec : Codec[RespawnInfo] = ( + uint(6) >>:~ { size => //max 63 + ("unk1" | PacketHelpers.listOfNSized(size, Vector3.codec_pos)) :: + ("unk2" | PacketHelpers.listOfNSized(size, bool)) + }).exmap[RespawnInfo] ({ + case _ :: a :: b :: HNil => + Attempt.Successful(RespawnInfo(a, b)) + }, + { + case RespawnInfo(a, b) => + val alen = a.length + if(alen != b.length) { + Attempt.Failure(Err(s"respawn info lists must match in length - $alen vs ${b.length}")) + } + else if(alen > 63) { + Attempt.Failure(Err(s"respawn info lists too long - $alen > 63")) + } + else { + Attempt.Successful(alen :: a :: b :: HNil) + } + } + ) + + /* + technically, the order of reading should be 16u + 1u + 7u which is byte-aligned + the 7u, however, is divided into a subsequent 1u + 6u reading + if that second 1u is true, the 6u doesn't matter and doesn't need to be read when not necessary + */ + implicit val codec : Codec[RespawnAMSInfoMessage] = ( + ("unk1" | PlanetSideGUID.codec) :: + ("unk2" | bool) :: + (bool >>:~ { test => + conditional(!test, "unk3" | info_codec).hlist + }) + ).xmap[RespawnAMSInfoMessage] ( + { + case u1 :: u2 :: _ :: u3 :: HNil => + RespawnAMSInfoMessage(u1, u2, u3) + }, + { + case RespawnAMSInfoMessage(u1, u2, u3) => + u1 :: u2 :: u3.isDefined :: u3 :: HNil + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala b/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala index c3049546..f73052cf 100644 --- a/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala @@ -10,7 +10,8 @@ import scodec.codecs._ * @param unk1 when defined, na; * non-zero when selecting the sanctuary option from a non-sanctuary continent deployment map * @param unk2 when defined, indicates type of spawn point by destination; - * 0 is unknown (may refer to all available spawns regardless of last position); + * 0 is nothing; + * 2 is ams; * 6 is towers; * 7 is facilities * @param unk3 na diff --git a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala new file mode 100644 index 00000000..3874b625 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala @@ -0,0 +1,78 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class WaypointEvent(unk1 : Int, + pos : Vector3, + unk2 : Int) + +final case class SquadWaypointEvent(unk1 : Int, + unk2 : Int, + unk3 : Long, + unk4 : Int, + unk5 : Option[Long], + unk6 : Option[WaypointEvent]) + extends PlanetSideGamePacket { + type Packet = SquadWaypointEvent + def opcode = GamePacketOpcode.SquadWaypointEvent + def encode = SquadWaypointEvent.encode(this) +} + +object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] { + def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Long) : SquadWaypointEvent = + SquadWaypointEvent(unk1, unk2, unk3, unk4, Some(unk_a), None) + + def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Int, pos : Vector3, unk_b : Int) : SquadWaypointEvent = + SquadWaypointEvent(unk1, unk2, unk3, unk4, None, Some(WaypointEvent(unk_a, pos, unk_b))) + + def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int) : SquadWaypointEvent = + SquadWaypointEvent(unk1, unk2, unk3, unk4, None, None) + + private val waypoint_codec : Codec[WaypointEvent] = ( + ("unk1" | uint16L) :: + ("pos" | Vector3.codec_pos) :: + ("unk2" | uint(3)) + ).as[WaypointEvent] + + implicit val codec : Codec[SquadWaypointEvent] = ( + ("unk1" | uint2) >>:~ { unk1 => + ("unk2" | uint16L) :: + ("unk3" | uint32L) :: + ("unk4" | uint8L) :: + ("unk5" | conditional(unk1 == 1, uint32L)) :: + ("unk6" | conditional(unk1 == 0, waypoint_codec)) + } + ).exmap[SquadWaypointEvent] ( + { + case 0 :: a :: b :: c :: None :: Some(d) :: HNil => + Attempt.Successful(SquadWaypointEvent(0, a, b, c, None, Some(d))) + + case 1 :: a :: b :: c :: Some(d) :: None :: HNil => + Attempt.Successful(SquadWaypointEvent(1, a, b, c, Some(d), None)) + + case a :: b :: c :: d :: None :: None :: HNil => + Attempt.Successful(SquadWaypointEvent(a, b, c, d, None, None)) + + case n :: _ :: _ :: _ :: _ :: _ :: HNil => + Attempt.Failure(Err(s"unexpected format for unk1 - $n")) + }, + { + case SquadWaypointEvent(0, a, b, c, None, Some(d)) => + Attempt.Successful(0 :: a :: b :: c :: None :: Some(d) :: HNil) + + case SquadWaypointEvent(1, a, b, c, Some(d), None) => + Attempt.Successful(1 :: a :: b :: c :: Some(d) :: None :: HNil) + + case SquadWaypointEvent(a, b, c, d, None, None) => + Attempt.Successful(a :: b :: c :: d :: None :: None :: HNil) + + case SquadWaypointEvent(n, _, _, _, _, _) => + Attempt.Failure(Err(s"unexpected format for unk1 - $n")) + } + ) +} diff --git a/common/src/test/scala/game/BattleplanMessageTest.scala b/common/src/test/scala/game/BattleplanMessageTest.scala index 0b015a70..811ebcd7 100644 --- a/common/src/test/scala/game/BattleplanMessageTest.scala +++ b/common/src/test/scala/game/BattleplanMessageTest.scala @@ -19,7 +19,7 @@ class BattleplanMessageTest extends Specification { case BattleplanMessage(char_id, player_name, zone_id, diagrams) => char_id mustEqual 41490746 player_name mustEqual "YetAnotherFailureAlt" - zone_id mustEqual PlanetSideGUID(0) + zone_id mustEqual 0 diagrams.size mustEqual 1 //0 diagrams.head.action mustEqual DiagramActionCode.StartDrawing @@ -34,7 +34,7 @@ class BattleplanMessageTest extends Specification { case BattleplanMessage(char_id, player_name, zone_id, diagrams) => char_id mustEqual 41490746 player_name mustEqual "YetAnotherFailureAlt" - zone_id mustEqual PlanetSideGUID(0) + zone_id mustEqual 0 diagrams.size mustEqual 1 //0 diagrams.head.action mustEqual DiagramActionCode.StopDrawing @@ -49,7 +49,7 @@ class BattleplanMessageTest extends Specification { case BattleplanMessage(char_id, player_name, zone_id, diagrams) => char_id mustEqual 41378949 player_name mustEqual "Outstabulous" - zone_id mustEqual PlanetSideGUID(10) + zone_id mustEqual 10 diagrams.size mustEqual 32 //0 diagrams.head.action mustEqual DiagramActionCode.Vertex @@ -191,7 +191,7 @@ class BattleplanMessageTest extends Specification { case BattleplanMessage(char_id, player_name, zone_id, diagrams) => char_id mustEqual 41378949 player_name mustEqual "Outstabulous" - zone_id mustEqual PlanetSideGUID(10) + zone_id mustEqual 10 diagrams.size mustEqual 3 //0 diagrams.head.action mustEqual DiagramActionCode.Style @@ -217,7 +217,7 @@ class BattleplanMessageTest extends Specification { case BattleplanMessage(char_id, player_name, zone_id, diagrams) => char_id mustEqual 41378949 player_name mustEqual "Outstabulous" - zone_id mustEqual PlanetSideGUID(10) + zone_id mustEqual 10 diagrams.size mustEqual 1 //0 diagrams.head.action mustEqual DiagramActionCode.DrawString @@ -237,7 +237,7 @@ class BattleplanMessageTest extends Specification { val msg = BattleplanMessage( 41490746, "YetAnotherFailureAlt", - PlanetSideGUID(0), + 0, BattleDiagramAction(DiagramActionCode.StartDrawing) :: Nil ) @@ -250,7 +250,7 @@ class BattleplanMessageTest extends Specification { val msg = BattleplanMessage( 41490746, "YetAnotherFailureAlt", - PlanetSideGUID(0), + 0, BattleDiagramAction(DiagramActionCode.StopDrawing) :: Nil ) @@ -263,7 +263,7 @@ class BattleplanMessageTest extends Specification { val msg = BattleplanMessage( 41378949, "Outstabulous", - PlanetSideGUID(10), + 10, BattleDiagramAction.vertex(7512.0f, 6312.0f) :: BattleDiagramAction.vertex(7512.0f, 6328.0f) :: BattleDiagramAction.vertex(7512.0f, 6344.0f) :: @@ -307,7 +307,7 @@ class BattleplanMessageTest extends Specification { val msg = BattleplanMessage( 41378949, "Outstabulous", - PlanetSideGUID(10), + 10, BattleDiagramAction.style(3.0f, 2) :: BattleDiagramAction.vertex(7512.0f, 6328.0f) :: BattleDiagramAction.vertex(7512.0f, 6344.0f) :: @@ -322,7 +322,7 @@ class BattleplanMessageTest extends Specification { val msg = BattleplanMessage( 41378949, "Outstabulous", - PlanetSideGUID(10), + 10, BattleDiagramAction.drawString(7512.0f, 6312.0f, 2, 0, "Hello Auraxis!") :: Nil ) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector diff --git a/common/src/test/scala/game/SquadWaypointEventTest.scala b/common/src/test/scala/game/SquadWaypointEventTest.scala new file mode 100644 index 00000000..3c2b8bb8 --- /dev/null +++ b/common/src/test/scala/game/SquadWaypointEventTest.scala @@ -0,0 +1,99 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game.{SquadWaypointEvent, WaypointEvent} +import net.psforever.types.Vector3 +import scodec.bits._ + +class SquadWaypointEventTest extends Specification { + val string_1 = hex"84 82c025d9b6c04000" + val string_2 = hex"84 8280000000000100" + val string_3 = hex"84 00c03f1e5e808042803f3018f316800008" + val string_4 = hex"84 40c03f1e5e80804100000000" //fabricated example + + "decode (1)" in { + PacketCoding.DecodePacket(string_1).require match { + case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => + unk1 mustEqual 2 + unk2 mustEqual 11 + unk3 mustEqual 31155863L + unk4 mustEqual 0 + unk5 mustEqual None + unk6 mustEqual None + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.DecodePacket(string_2).require match { + case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => + unk1 mustEqual 2 + unk2 mustEqual 10 + unk3 mustEqual 0L + unk4 mustEqual 4 + unk5 mustEqual None + unk6 mustEqual None + case _ => + ko + } + } + + "decode (3)" in { + PacketCoding.DecodePacket(string_3).require match { + case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => + unk1 mustEqual 0 + unk2 mustEqual 3 + unk3 mustEqual 41581052L + unk4 mustEqual 1 + unk5 mustEqual None + unk6 mustEqual Some(WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1)) + case _ => + ko + } + } + + "decode (4)" in { + PacketCoding.DecodePacket(string_4).require match { + case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => + unk1 mustEqual 1 + unk2 mustEqual 3 + unk3 mustEqual 41581052L + unk4 mustEqual 1 + unk5 mustEqual Some(4L) + unk6 mustEqual None + case _ => + ko + } + } + + "encode (1)" in { + val msg = SquadWaypointEvent(2, 11, 31155863L, 0) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_1 + } + + "encode (2)" in { + val msg = SquadWaypointEvent(2, 10, 0L, 4) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_2 + } + + "encode (3)" in { + val msg = SquadWaypointEvent(0, 3, 41581052L, 1, 10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_3 + } + + "encode (4)" in { + val msg = SquadWaypointEvent(1, 3, 41581052L, 1, 4L) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_4 + } +} diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 251694f3..6137b993 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -194,7 +194,6 @@ class ZoneActorTest extends ActorTest { val reply1 = receiveOne(Duration.create(200, "ms")) assert(reply1.isInstanceOf[Zone.Lattice.SpawnPoint]) assert(reply1.asInstanceOf[Zone.Lattice.SpawnPoint].zone_id == "test") - assert(reply1.asInstanceOf[Zone.Lattice.SpawnPoint].building == bldg1) assert(reply1.asInstanceOf[Zone.Lattice.SpawnPoint].spawn_tube.Owner == bldg1) player.Position = Vector3(3,3,3) //closer to bldg3 @@ -202,7 +201,6 @@ class ZoneActorTest extends ActorTest { val reply3 = receiveOne(Duration.create(200, "ms")) assert(reply3.isInstanceOf[Zone.Lattice.SpawnPoint]) assert(reply3.asInstanceOf[Zone.Lattice.SpawnPoint].zone_id == "test") - assert(reply3.asInstanceOf[Zone.Lattice.SpawnPoint].building == bldg3) assert(reply3.asInstanceOf[Zone.Lattice.SpawnPoint].spawn_tube.Owner == bldg3) } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 05075da1..8ec79f16 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -4,7 +4,7 @@ import java.util.concurrent.atomic.AtomicInteger import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} import net.psforever.packet._ import net.psforever.packet.control._ -import net.psforever.packet.game._ +import net.psforever.packet.game.{BattleDiagramAction, _} import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC @@ -76,6 +76,8 @@ class WorldSessionActor extends Actor with MDCContextAware { var deadState : DeadState.Value = DeadState.Dead var whenUsedLastKit : Long = 0 + var amsSpawnPoint : Option[SpawnTube] = None + var clientKeepAlive : Cancellable = DefaultCancellable.obj var progressBarUpdate : Cancellable = DefaultCancellable.obj var reviveTimer : Cancellable = DefaultCancellable.obj @@ -546,16 +548,43 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + case VehicleResponse.UpdateAmsSpawnPoint(list) => + if(player.isBackpack) { + //dismiss old ams spawn point + ClearCurrentAmsSpawnPoint() + //draw new ams spawn point + list + .filter(tube => tube.Faction == player.Faction) + .sortBy(tube => Vector3.DistanceSquared(tube.Position, player.Position)) + .headOption match { + case Some(tube) => + sendResponse( + BattleplanMessage(41378949, "ams", continent.Number, List(BattleDiagramAction(DiagramActionCode.StartDrawing))) + ) + sendResponse( + BattleplanMessage(41378949, "ams", continent.Number, List(BattleDiagramAction.drawString(tube.Position.x, tube.Position.y, 3, 0, "AMS"))) + ) + amsSpawnPoint = Some(tube) + case None => ; + } + } + case _ => ; } case Deployment.CanDeploy(obj, state) => val vehicle_guid = obj.GUID - if(state == DriveState.Deploying) { + //TODO remove this arbitrary allowance angle when no longer helpful + if(obj.Orientation.x > 30 && obj.Orientation.x < 330) { + obj.DeploymentState = DriveState.Mobile + CanNotChangeDeployment(obj, state, "ground too steep") + } + else if(state == DriveState.Deploying) { log.info(s"DeployRequest: $obj transitioning to deploy state") obj.Velocity = Some(Vector3.Zero) //no velocity sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) + DeploymentActivities(obj) import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global context.system.scheduler.scheduleOnce(obj.DeployTime milliseconds, obj.Actor, Deployment.TryDeploy(DriveState.Deployed)) @@ -577,6 +606,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"DeployRequest: $obj transitioning to undeploy state") sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) + DeploymentActivities(obj) import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global context.system.scheduler.scheduleOnce(obj.UndeployTime milliseconds, obj.Actor, Deployment.TryUndeploy(DriveState.Mobile)) @@ -585,6 +615,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"DeployRequest: $obj is Mobile") sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) + DeploymentActivities(obj) //... } else { @@ -1247,10 +1278,41 @@ class WorldSessionActor extends Actor with MDCContextAware { case Zone.Population.PlayerAlreadySpawned(zone, tplayer) => log.warn(s"$tplayer is already spawned on zone ${zone.Id}; a clerical error?") - case Zone.Lattice.SpawnPoint(zone_id, building, spawn_tube) => - log.info(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id in ${building.Id} @ ${spawn_tube.GUID.guid} selected") + case Zone.Lattice.SpawnPoint(zone_id, spawn_tube) => + var pos = spawn_tube.Position + var ori = spawn_tube.Orientation + spawn_tube.Owner match { + case building : Building => + log.info(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id in building ${building.Id} selected") + case vehicle : Vehicle => + //TODO replace this bad math with good math or no math + //position the player alongside either of the AMS's terminals, facing away from it + val side = if(System.currentTimeMillis() % 2 == 0) 1 else -1 //right | left + val z = spawn_tube.Orientation.z + val zrot = (z + 90) % 360 + val x = spawn_tube.Orientation.x + val xsin = 3 * side * math.abs(math.sin(math.toRadians(x))).toFloat + 0.5f //sin because 0-degrees is up + val zrad = math.toRadians(zrot) + pos = pos + (Vector3(math.sin(zrad).toFloat, math.cos(zrad).toFloat, 0) * (3 * side)) //x=sin, y=cos because compass-0 is East, not North + ori = if(side == 1) { + Vector3(0, 0, zrot) + } + else { + Vector3(0, 0, (z - 90) % 360) + } + pos = if(x >= 330) { //leaning to the left + pos + Vector3(0, 0, xsin) + } + else { + pos - Vector3(0, 0, xsin) + } + log.info(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id at ams ${vehicle.GUID.guid} selected") + case owner => + log.warn(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id at ${spawn_tube.Position} has unexpected owner $owner") + } respawnTimer.cancel reviveTimer.cancel + ClearCurrentAmsSpawnPoint() val sameZone = zone_id == continent.Id val backpack = player.isBackpack val respawnTime : Long = if(sameZone) { 10 } else { 0 } //s @@ -1267,8 +1329,8 @@ class WorldSessionActor extends Actor with MDCContextAware { player //player is deconstructing self } - tplayer.Position = spawn_tube.Position - tplayer.Orientation = spawn_tube.Orientation + tplayer.Position = pos + tplayer.Orientation = ori val (target, msg) : (ActorRef, Any) = if(sameZone) { if(backpack) { //respawning from unregistered player @@ -1305,7 +1367,13 @@ class WorldSessionActor extends Actor with MDCContextAware { case Zone.Lattice.NoValidSpawnPoint(zone_number, Some(spawn_group)) => log.warn(s"Zone.Lattice.SpawnPoint: zone $zone_number has no available ${player.Faction} targets in spawn group $spawn_group") reviveTimer.cancel - RequestSanctuaryZoneSpawn(player, zone_number) + if(spawn_group == 2) { + sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "No friendly AMS is deployed in this region.", None)) + galaxy ! Zone.Lattice.RequestSpawnPoint(zone_number, player, 0) + } + else { + RequestSanctuaryZoneSpawn(player, zone_number) + } case InterstellarCluster.ClientInitializationComplete() => StopBundlingPackets() @@ -1486,7 +1554,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //TODO begin temp player character auto-loading; remove later import net.psforever.objects.GlobalDefinitions._ import net.psforever.types.CertificationType._ - avatar = Avatar("TestCharacter"+sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + avatar = Avatar("TestCharacter" + sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) avatar.Certifications += StandardAssault avatar.Certifications += MediumAssault avatar.Certifications += StandardExoSuit @@ -1539,6 +1607,7 @@ class WorldSessionActor extends Actor with MDCContextAware { import scala.concurrent.ExecutionContext.Implicits.global clientKeepAlive.cancel clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient()) + log.warn(PacketCoding.DecodePacket(hex"d2327e7b8a972b95113881003710").toString) case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => log.info("Handling " + msg) @@ -1571,7 +1640,6 @@ class WorldSessionActor extends Actor with MDCContextAware { vehicleService ! Service.Join(continent.Id) configZone(continent) sendResponse(TimeOfDayMessage(1191182336)) - //custom sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary." sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list @@ -1597,7 +1665,9 @@ class WorldSessionActor extends Actor with MDCContextAware { } }) //load corpses in zone - continent.Corpses.foreach { TurnPlayerIntoCorpse } + continent.Corpses.foreach { + TurnPlayerIntoCorpse + } //load active vehicles in zone continent.Vehicles.foreach(vehicle => { val definition = vehicle.Definition @@ -1615,7 +1685,7 @@ class WorldSessionActor extends Actor with MDCContextAware { ReloadVehicleAccessPermissions(vehicle) }) //implant terminals - continent.Map.TerminalToInterface.foreach({ case((terminal_guid, interface_guid)) => + continent.Map.TerminalToInterface.foreach({ case ((terminal_guid, interface_guid)) => val parent_guid = PlanetSideGUID(terminal_guid) continent.GUID(interface_guid) match { case Some(obj : Terminal) => @@ -1633,7 +1703,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //seat terminal occupants continent.GUID(terminal_guid) match { case Some(obj : Mountable) => - obj.MountPoints.foreach({ case((_, seat_num)) => + obj.MountPoints.foreach({ case ((_, seat_num)) => obj.Seat(seat_num).get.Occupant match { case Some(tplayer) => if(tplayer.HasGUID) { @@ -1656,7 +1726,6 @@ class WorldSessionActor extends Actor with MDCContextAware { player.FacingYawUpper = yaw_upper player.Crouching = is_crouching player.Jumping = is_jumping - if(vel.isDefined && usingMedicalTerminal.isDefined) { StopUsingProximityUnit(continent.GUID(usingMedicalTerminal.get).get.asInstanceOf[ProximityTerminal]) } @@ -1692,16 +1761,17 @@ class WorldSessionActor extends Actor with MDCContextAware { log.warn(s"ChildObjectState: player $player's controllable agent not available in scope") } case None => - //TODO status condition of "playing getting out of vehicle to allow for late packets without warning - //log.warn(s"ChildObjectState: player $player not related to anything with a controllable agent") + //TODO status condition of "playing getting out of vehicle to allow for late packets without warning + //log.warn(s"ChildObjectState: player $player not related to anything with a controllable agent") } - //log.info("ChildObjectState: " + msg) + //log.info("ChildObjectState: " + msg) case msg @ VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk5, unk6, unk7, wheels, unk9, unkA) => continent.GUID(vehicle_guid) match { case Some(obj : Vehicle) => val seat = obj.Seat(0).get - if(seat.Occupant.contains(player)) { //we're driving the vehicle + if(seat.Occupant.contains(player)) { + //we're driving the vehicle player.Position = pos //convenient if(seat.ControlledWeapon.isEmpty) { player.Orientation = Vector3(0f, 0f, ang.z) //convenient @@ -1711,17 +1781,17 @@ class WorldSessionActor extends Actor with MDCContextAware { obj.Velocity = vel vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, unk5, unk6, unk7, wheels, unk9, unkA)) } - //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle + //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle case _ => log.warn(s"VehicleState: no vehicle $vehicle_guid found in zone") } //log.info(s"VehicleState: $msg") case msg @ VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) => - //log.info(s"VehicleSubState: $vehicle_guid, $player_guid, $vehicle_pos, $vehicle_ang, $vel, $unk1, $unk2") + //log.info(s"VehicleSubState: $vehicle_guid, $player_guid, $vehicle_pos, $vehicle_ang, $vel, $unk1, $unk2") case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vector, unk1, unk2, unk3, unk4, time_alive) => - //log.info("ProjectileState: " + msg) + //log.info("ProjectileState: " + msg) case msg @ ReleaseAvatarRequestMessage() => log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released") @@ -1730,6 +1800,7 @@ class WorldSessionActor extends Actor with MDCContextAware { deadState = DeadState.Release sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) continent.Population ! Zone.Population.Release(avatar) + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.UpdateAmsSpawnPoint(continent)) player.VehicleSeated match { case None => FriskCorpse(player) @@ -1738,7 +1809,8 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Population ! Zone.Corpse.Add(player) //TODO move back out of this match case when changing below issue avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) } - else { //no items in inventory; leave no corpse + else { + //no items in inventory; leave no corpse val player_guid = player.GUID sendResponse(ObjectDeleteMessage(player_guid, 0)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) @@ -1769,20 +1841,22 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info("SetChatFilters: " + msg) case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) => + var makeReply : Boolean = true var echoContents : String = contents + val trimContents = contents.trim //TODO messy on/off strings may work if(messagetype == ChatMessageType.CMT_FLY) { - if(contents.trim.equals("on")) { + if(trimContents.equals("on")) { flying = true } - else if(contents.trim.equals("off")) { + else if(trimContents.equals("off")) { flying = false } } else if(messagetype == ChatMessageType.CMT_SPEED) { speed = { try { - contents.trim.toFloat + trimContents.toFloat } catch { case _ : Exception => @@ -1792,7 +1866,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } } else if(messagetype == ChatMessageType.CMT_TOGGLESPECTATORMODE) { - if(contents.trim.equals("on")) { + if(trimContents.equals("on")) { spectator = true } else if(contents.trim.equals("off")) { @@ -1826,18 +1900,19 @@ class WorldSessionActor extends Actor with MDCContextAware { case (false, _) => ; } - + // TODO: Prevents log spam, but should be handled correctly - if (messagetype != ChatMessageType.CMT_TOGGLE_GM) { + if(messagetype != ChatMessageType.CMT_TOGGLE_GM) { log.info("Chat: " + msg) } - + else { + makeReply = false + } if(messagetype == ChatMessageType.CMT_SUICIDE) { if(player.isAlive && deadState != DeadState.Release) { KillPlayer(player) } } - if(messagetype == ChatMessageType.CMT_DESTROY) { val guid = contents.toInt continent.Map.TerminalToSpawnPad.get(guid) match { @@ -1847,25 +1922,30 @@ class WorldSessionActor extends Actor with MDCContextAware { self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid))) } } - - if (messagetype == ChatMessageType.CMT_VOICE) { + if(messagetype == ChatMessageType.CMT_VOICE) { sendResponse(ChatMsg(ChatMessageType.CMT_VOICE, false, player.Name, contents, None)) } - // TODO: handle this appropriately if(messagetype == ChatMessageType.CMT_QUIT) { sendResponse(DropCryptoSession()) sendResponse(DropSession(sessionId, "user quit")) } - - if(contents.trim.equals("!loc")) { //dev hack; consider bang-commands to complement slash-commands in future + //dev hack; consider bang-commands to complement slash-commands in future + if(trimContents.equals("!loc")) { echoContents = s"zone=${continent.Id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}" log.info(echoContents) } - + else if(trimContents.equals("!ams")) { + makeReply = false + if(player.isBackpack) { //player is on deployment screen (either dead or deconstructed) + galaxy ! Zone.Lattice.RequestSpawnPoint(continent.Number, player, 2) + } + } // TODO: Depending on messagetype, may need to prepend sender's name to contents with proper spacing // TODO: Just replays the packet straight back to sender; actually needs to be routed to recipients! - sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, echoContents, note_contents)) + if(makeReply) { + sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, echoContents, note_contents)) + } case msg @ VoiceHostRequest(unk, PlanetSideGUID(player_guid), data) => log.info("Player "+player_guid+" requested in-game voice chat.") @@ -3989,7 +4069,19 @@ class WorldSessionActor extends Actor with MDCContextAware { obj match { case vehicle : Vehicle => ReloadVehicleAccessPermissions(vehicle) //TODO we should not have to do this imho - sendResponse(PlanetsideAttributeMessage(obj.GUID, 81, 1)) + // + if(obj.Definition == GlobalDefinitions.ams) { + obj.DeploymentState match { + case DriveState.Deployed => + vehicleService ! VehicleServiceMessage.AMSDeploymentChange(continent) + sendResponse(PlanetsideAttributeMessage(obj.GUID, 81, 1)) + case DriveState.Undeploying => + vehicleService ! VehicleServiceMessage.AMSDeploymentChange(continent) + sendResponse(PlanetsideAttributeMessage(obj.GUID, 81, 0)) + case DriveState.Mobile | DriveState.State7 => + case _ => ; + } + } case _ => ; } } @@ -4013,6 +4105,17 @@ class WorldSessionActor extends Actor with MDCContextAware { log.error(s"DeployRequest: $obj can not transition to $state - $reason$mobileShift") } + def ClearCurrentAmsSpawnPoint() : Unit = { + amsSpawnPoint match { + case Some(_) => + sendResponse( + BattleplanMessage(41378949, "ams", continent.Number, List(BattleDiagramAction(DiagramActionCode.StopDrawing))) + ) + amsSpawnPoint = None + case None => ; + } + } + /** * For a given continental structure, determine the method of generating server-join client configuration packets. * @param continentNumber the zone id diff --git a/pslogin/src/main/scala/services/vehicle/VehicleAction.scala b/pslogin/src/main/scala/services/vehicle/VehicleAction.scala index c876f340..68c9788e 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleAction.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleAction.scala @@ -25,4 +25,6 @@ object VehicleAction { final case class UnloadVehicle(player_guid : PlanetSideGUID, continent : Zone, vehicle : Vehicle) extends Action final case class UnstowEquipment(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID) extends Action final case class VehicleState(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, unk1 : Int, pos : Vector3, ang : Vector3, vel : Option[Vector3], unk2 : Option[Int], unk3 : Int, unk4 : Int, wheel_direction : Int, unk5 : Boolean, unk6 : Boolean) extends Action + + final case class UpdateAmsSpawnPoint(zone : Zone) extends Action } diff --git a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala index e29437cf..f3c9f9eb 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package services.vehicle +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.{PlanetSideGameObject, Vehicle} import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.objectcreate.ConstructorData @@ -28,4 +29,6 @@ object VehicleResponse { final case class UnloadVehicle(vehicle_guid : PlanetSideGUID) extends Response final case class UnstowEquipment(item_guid : PlanetSideGUID) extends Response final case class VehicleState(vehicle_guid : PlanetSideGUID, unk1 : Int, pos : Vector3, ang : Vector3, vel : Option[Vector3], unk2 : Option[Int], unk3 : Int, unk4 : Int, wheel_direction : Int, unk5 : Boolean, unk6 : Boolean) extends Response + + final case class UpdateAmsSpawnPoint(list : List[SpawnTube]) extends Response } diff --git a/pslogin/src/main/scala/services/vehicle/VehicleService.scala b/pslogin/src/main/scala/services/vehicle/VehicleService.scala index 8fb490cd..6b16ec2b 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleService.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleService.scala @@ -5,6 +5,8 @@ import akka.actor.{Actor, ActorRef, Props} import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.zones.Zone import services.vehicle.support.{DeconstructionActor, DelayedDeconstructionActor} +import net.psforever.types.DriveState + import services.{GenericEventBus, Service} class VehicleService extends Actor { @@ -93,6 +95,10 @@ class VehicleService extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.VehicleState(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6)) ) + + //unlike other messages, just return to sender, don't publish + case VehicleAction.UpdateAmsSpawnPoint(zone : Zone) => + sender ! VehicleServiceResponse(s"/$forChannel/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UpdateAmsSpawnPoint(AmsSpawnPoints(zone))) case _ => ; } @@ -161,7 +167,23 @@ class VehicleService extends Actor { vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle.GUID) vehicleDecon ! DeconstructionActor.RequestDeleteVehicle(vehicle, zone) + //correspondence from WorldSessionActor + case VehicleServiceMessage.AMSDeploymentChange(zone) => + VehicleEvents.publish( + VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UpdateAmsSpawnPoint(AmsSpawnPoints(zone))) + ) + case msg => log.info(s"Unhandled message $msg from $sender") } + + import net.psforever.objects.serverobject.tube.SpawnTube + def AmsSpawnPoints(zone : Zone) : List[SpawnTube] = { + import net.psforever.objects.vehicles.UtilityType + import net.psforever.objects.GlobalDefinitions + zone.Vehicles + .filter(veh => veh.Definition == GlobalDefinitions.ams && veh.DeploymentState == DriveState.Deployed) + .flatMap(veh => veh.Utilities.values.filter(util => util.UtilType == UtilityType.ams_respawn_tube) ) + .map(util => util().asInstanceOf[SpawnTube]) + } } diff --git a/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala b/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala index 48c03dab..61ec6854 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala @@ -13,4 +13,6 @@ object VehicleServiceMessage { final case class RevokeActorControl(vehicle : Vehicle) final case class RequestDeleteVehicle(vehicle : Vehicle, continent : Zone) final case class UnscheduleDeconstruction(vehicle_guid : PlanetSideGUID) + + final case class AMSDeploymentChange(zone : Zone) } diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala index 906e7e9c..2a95b8a5 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -2,7 +2,7 @@ package services.vehicle.support import akka.actor.{Actor, ActorRef, Cancellable} -import net.psforever.objects.{DefaultCancellable, Vehicle} +import net.psforever.objects.{DefaultCancellable, GlobalDefinitions, Vehicle} import net.psforever.objects.guid.TaskResolver import net.psforever.objects.vehicles.Seat import net.psforever.objects.zones.Zone @@ -96,9 +96,20 @@ class DeconstructionActor extends Actor { heapEmptyProcess.cancel val now : Long = System.nanoTime val (vehiclesToScrap, vehiclesRemain) = PartitionEntries(vehicles, now) - vehicles = vehiclesRemain //entries from original list before partition + vehicles = vehiclesRemain vehicleScrapHeap = vehicleScrapHeap ++ vehiclesToScrap //may include existing entries - vehiclesToScrap.foreach { RetirementTask } + vehiclesToScrap.foreach(entry => { + val vehicle = entry.vehicle + val zone = entry.zone + RetirementTask(entry) + if(vehicle.Definition == GlobalDefinitions.ams) { + import net.psforever.types.DriveState + vehicle.DeploymentState = DriveState.Mobile //internally undeployed //TODO this should be temporary? + context.parent ! VehicleServiceMessage.AMSDeploymentChange(zone) + } + taskResolver ! DeconstructionTask(vehicle, zone) + }) + if(vehiclesRemain.nonEmpty) { val short_timeout : FiniteDuration = math.max(1, DeconstructionActor.timeout_time - (now - vehiclesRemain.head.time)) nanoseconds import scala.concurrent.ExecutionContext.Implicits.global @@ -128,6 +139,7 @@ class DeconstructionActor extends Actor { def RetirementTask(entry : DeconstructionActor.VehicleEntry) : Unit = { val vehicle = entry.vehicle val zone = entry.zone + vehicle.Position = Vector3.Zero //somewhere it will not disturb anything zone.Transport ! Zone.Vehicle.Despawn(vehicle) context.parent ! DeconstructionActor.DeleteVehicle(vehicle.GUID, zone.Id) //call up to the main event system } @@ -135,7 +147,6 @@ class DeconstructionActor extends Actor { def DestructionTask(entry : DeconstructionActor.VehicleEntry) : Unit = { val vehicle = entry.vehicle val zone = entry.zone - vehicle.Position = Vector3.Zero //somewhere it will not disturb anything taskResolver ! DeconstructionTask(vehicle, zone) }