diff --git a/.codecov.yml b/.codecov.yml index d7acf34c4..ca48a57c3 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -21,6 +21,7 @@ ignore: - "src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala" - "src/main/scala/net/psforever/objects/serverobject/pad/AutoDriveControls.scala" - "src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala" + - "src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala" - "src/main/scala/net/psforever/objects/serverobject/turret/TurretUpgrade.scala" - "src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala" - "src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala" @@ -70,6 +71,8 @@ ignore: - "src/main/scala/net/psforever/services/avatar/AvatarResponse.scala" - "src/main/scala/net/psforever/services/galaxy/GalaxyAction.scala" - "src/main/scala/net/psforever/services/galaxy/GalaxyResponse.scala" + - "src/main/scala/net/psforever/services/hart/HartEvent.scala" + - "src/main/scala/net/psforever/services/hart/HartTimerActions.scala" - "src/main/scala/net/psforever/services/local/LocalAction.scala" - "src/main/scala/net/psforever/services/local/LocalResponse.scala" - "src/main/scala/net/psforever/services/vehicle/VehicleAction.scala" diff --git a/server/src/main/scala/net/psforever/server/Server.scala b/server/src/main/scala/net/psforever/server/Server.scala index 5aeae7c6a..9612fc2d7 100644 --- a/server/src/main/scala/net/psforever/server/Server.scala +++ b/server/src/main/scala/net/psforever/server/Server.scala @@ -36,6 +36,7 @@ import org.slf4j import scopt.OParser import akka.actor.typed.scaladsl.adapter._ import net.psforever.packet.PlanetSidePacket +import net.psforever.services.hart.HartService object Server { private val logger = org.log4s.getLogger @@ -129,6 +130,7 @@ object Server { serviceManager ! ServiceManager.Register(classic.Props[SquadService](), "squad") serviceManager ! ServiceManager.Register(classic.Props[AccountPersistenceService](), "accountPersistence") serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager") + serviceManager ! ServiceManager.Register(classic.Props[HartService](), "hart") system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket") system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket") diff --git a/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala index 7da3afeef..24b3c144d 100644 --- a/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala +++ b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala @@ -203,7 +203,7 @@ class AutoRepairFacilityIntegrationAntGiveNtuTest extends FreedContextActorTest ant.NtuCapacitor = maxNtuCap ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Zone = zone - ant.Seats(0).Occupant = player + ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed building.Amenities = terminal building.Amenities = silo @@ -297,7 +297,7 @@ class AutoRepairFacilityIntegrationTerminalDestroyedTerminalAntTest extends Free ant.NtuCapacitor = maxNtuCap ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Zone = zone - ant.Seats(0).Occupant = player + ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed building.Amenities = terminal building.Amenities = silo @@ -399,7 +399,7 @@ class AutoRepairFacilityIntegrationTerminalIncompleteRepairTest extends FreedCon ant.NtuCapacitor = maxNtuCap ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Zone = zone - ant.Seats(0).Occupant = player + ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed building.Amenities = terminal building.Amenities = silo diff --git a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala index 43fafd4f5..79a2fa0a3 100644 --- a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala +++ b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala @@ -39,7 +39,7 @@ class VehicleSpawnControl2Test extends ActorTest { probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.LoadVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) - vehicle.Seats(0).Occupant = player + vehicle.Seats(0).mount(player) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.PlayerSeatedInVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ServerVehicleOverrideStart]) @@ -75,7 +75,7 @@ class VehicleSpawnControl3Test extends ActorTest { probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.LoadVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) - vehicle.Seats(0).Occupant = player + vehicle.Seats(0).mount(player) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.PlayerSeatedInVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ServerVehicleOverrideStart]) @@ -92,7 +92,7 @@ class VehicleSpawnControl3Test extends ActorTest { //if we move the vehicle away from the pad, we should receive a second ConcealPlayer message //that means that the first order has cleared and the spawn pad is now working on the second order successfully player.VehicleSeated = None //since shared between orders, as necessary - vehicle.Seats(0).Occupant = None + vehicle.Seats(0).unmount(player) vehicle.Position = Vector3(12, 0, 0) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ResetSpawnPad]) probe.expectMsgClass(3 seconds, classOf[VehicleSpawnPad.ConcealPlayer]) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index ae2052134..0bc62ab18 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -82,6 +82,15 @@ game { # Modify the amount of NTU drain per autorepair tick for facility amenities amenity-autorepair-drain-rate = 0.5 + # HART system, shuttles and facilities + hart { + # How long the shuttle is not boarding passengers (going through the motions) + in-flight-duration = 225000 + + # How long the shuttle allows passengers to board + boarding-duration = 60000 + } + new-avatar { # Starting battle rank br = 1 diff --git a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala index bd56e9f8f..470ca4040 100644 --- a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala +++ b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala @@ -420,7 +420,7 @@ class MiddlewareActor( case Successful((packet, None)) => in(packet) case Failure(e) => - log.error(s"could not decode packet: $e") + log.error(s"Could not decode packet: $e") } Behaviors.same @@ -530,7 +530,7 @@ class MiddlewareActor( def in(packet: Attempt[PlanetSidePacket]): Unit = { packet match { case Successful(_packet) => in(_packet) - case Failure(cause) => log.error(cause.message) + case Failure(cause) => log.error(s"Could not decode packet: ${cause.message}") } } @@ -543,7 +543,7 @@ class MiddlewareActor( case _ => PacketCoding.encodePacket(packet) match { case Successful(payload) => outQueue.enqueue((packet, payload)) - case Failure(cause) => log.error(cause.message) + case Failure(cause) => log.error(s"Could not encode $packet: ${cause.message}") } } } @@ -615,7 +615,7 @@ class MiddlewareActor( outQueueBundled.enqueue(smp(slot = 0, data.bytes)) sendFirstBundle() case Failure(cause) => - log.error(cause.message) + log.error(s"could not bundle $bundle: ${cause.message}") //to avoid packets being lost, unwrap bundle and queue the packets individually bundle.foreach { packet => outQueueBundled.enqueue(smp(slot = 0, packet.bytes)) @@ -626,7 +626,7 @@ class MiddlewareActor( } } catch { case e: Throwable => - log.error(s"outbound queue processing error - ${Option(e.getMessage).getOrElse(e.getClass.getSimpleName)}") + log.error(s"Outbound queue processing error: ${Option(e.getMessage).getOrElse(e.getClass.getSimpleName)}") } } @@ -901,7 +901,7 @@ class MiddlewareActor( case Successful(data) => data.grouped((MTU - 8) * 8).map(vec => smp(slot = 4, vec.bytes)).toSeq case Failure(cause) => - log.error(cause.message) + log.error(s"Could not split packet: ${cause.message}") Seq() } } else { diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 1088fa8cb..b5898ba8d 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -7,7 +7,6 @@ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} import akka.pattern.ask import akka.util.Timeout -import java.util.concurrent.TimeUnit import net.psforever.actors.net.MiddlewareActor import net.psforever.services.ServiceManager.Lookup import net.psforever.objects.locker.LockerContainer @@ -44,7 +43,7 @@ import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals._ -import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminals} +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} @@ -71,8 +70,9 @@ import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMes import net.psforever.services.properties.PropertyOverrideManager import net.psforever.services.support.SupportActor import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} +import net.psforever.services.hart.HartTimer import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} -import net.psforever.services.{InterstellarClusterService, RemoverActor, Service, ServiceManager} +import net.psforever.services.{RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS} import net.psforever.types._ import net.psforever.util.{Config, DefinitionUtil} import net.psforever.zones.Zones @@ -176,7 +176,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var galaxyService: ActorRef = ActorRef.noSender var squadService: ActorRef = ActorRef.noSender var propertyOverrideManager: ActorRef = Actor.noSender - var cluster: typed.ActorRef[InterstellarClusterService.Command] = Actor.noSender + var cluster: typed.ActorRef[ICS.Command] = Actor.noSender var _session: Session = Session() var progressBarValue: Option[Float] = None var shooting: Option[PlanetSideGUID] = None //ChangeFireStateMessage_Start @@ -240,9 +240,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** Upstream message counter
* Checks for server acknowledgement of the following messages in the following conditions:
* `PlayerStateMessageUpstream` (infantry)
- * `VehicleStateMessage` (driver seat only)
- * `ChildObjectStateMessage` (any gunner seat that is not the driver)
- * `KeepAliveMessage` (any passenger seat that is not the driver)
+ * `VehicleStateMessage` (driver mount only)
+ * `ChildObjectStateMessage` (any gunner mount that is not the driver)
+ * `KeepAliveMessage` (any passenger mount that is not the driver)
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second */ var upstreamMessageCount: Int = 0 @@ -300,7 +300,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con serviceManager ! Lookup("propertyOverrideManager") ServiceManager.receptionist ! Receptionist.Find( - InterstellarClusterService.InterstellarClusterServiceKey, + ICS.InterstellarClusterServiceKey, context.self ) @@ -385,7 +385,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con propertyOverrideManager = endpoint log.info("ID: " + session.id + " Got propertyOverrideManager service " + endpoint) - case InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings) => + case ICS.InterstellarClusterServiceKey.Listing(listings) => cluster = listings.head // Avatar subscription update @@ -408,7 +408,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case AvatarActor.AvatarLoginResponse(avatar) => session = session.copy(avatar = avatar) Deployables.InitializeDeployableQuantities(avatar) - cluster ! InterstellarClusterService.FilterZones(_ => true, context.self) + cluster ! ICS.FilterZones(_ => true, context.self) case packet: PlanetSideGamePacket => handleGamePkt(packet) @@ -436,7 +436,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con zoningChatMessageType = ChatMessageType.CMT_RECALL zoningStatus = Zoning.Status.Request beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetRandomSpawnPoint( + cluster ! ICS.GetRandomSpawnPoint( Zones.sanctuaryZoneNumber(player.Faction), player.Faction, Seq(SpawnGroup.Sanctuary), @@ -451,24 +451,24 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /* TODO no ask or adapters from classic to typed so this logic is happening in SpawnPointResponse implicit val timeout = Timeout(1 seconds) val future = - ask(cluster.toClassic, InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self)) - .mapTo[InterstellarClusterService.SpawnPointResponse] + ask(cluster.toClassic, ICS.GetInstantActionSpawnPoint(player.Faction, context.self)) + .mapTo[ICS.SpawnPointResponse] Await.result(future, 2 second) match { - case InterstellarClusterService.SpawnPointResponse(None) => + case ICS.SpawnPointResponse(None) => sendResponse( ChatMsg(ChatMessageType.CMT_INSTANTACTION, false, "", "@InstantActionNoHotspotsAvailable", None) ) - case InterstellarClusterService.SpawnPointResponse(Some(_)) => + case ICS.SpawnPointResponse(Some(_)) => beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) }) } beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) }) */ - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) case Quit() => //priority to quitting is given to quit over other zoning methods @@ -558,37 +558,32 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest) => (manifest.passengers.find { case (name, _) => player.Name.equals(name) } match { - case Some((name, index)) if vehicle.Seats(index).Occupant.isEmpty => - vehicle.Seats(index).Occupant = player + case Some((name, index)) if vehicle.Seats(index).occupant.isEmpty => + vehicle.Seats(index).mount(player) Some(vehicle) case Some((name, index)) => - log.warn(s"TransferPassenger: seat $index is already occupied") + log.warn(s"TransferPassenger: mount $index is already occupied") None case None => None }).orElse(manifest.cargo.find { case (name, _) => player.Name.equals(name) } match { case Some((name, index)) => - vehicle.CargoHolds(index).Occupant match { + vehicle.CargoHolds(index).occupant match { case Some(cargo) => - cargo.Seats(0).Occupant match { - case Some(driver) if driver.Name.equals(name) => - Some(cargo) - case _ => - None - } + cargo.Seats(0).occupants.find(_.Name.equals(name)) case None => None } case None => None }) match { - case Some(v) => + case Some(v: Vehicle) => galaxyService ! Service.Leave(Some(temp_channel)) //temporary vehicle-specific channel (see above) deadState = DeadState.Release sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds) - case None => + case _ => interstellarFerry match { case None => galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones @@ -956,14 +951,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Zone.Population.PlayerAlreadySpawned(zone, tplayer) => log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; a clerical error?") - case InterstellarClusterService.SpawnPointResponse(response) => + case ICS.SpawnPointResponse(response) => zoningType match { case Zoning.Method.InstantAction if response.isEmpty => CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable") case Zoning.Method.InstantAction if zoningStatus == Zoning.Status.Request => beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) }) case zoningType => @@ -992,9 +987,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } + case ICS.DroppodLaunchDenial(errorCode, _) => + sendResponse(DroppodLaunchResponseMessage(errorCode, player.GUID)) + + case ICS.DroppodLaunchConfirmation(zone, position) => + LoadZoneLaunchDroppod(zone, position) + + case msg @ Zone.Vehicle.HasSpawned(zone, vehicle) => ; + case msg @ Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) => log.warn(s"$msg") + case msg @ Zone.Vehicle.HasDespawned(zone, vehicle) => ; + case msg @ Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) => log.warn(s"$msg") @@ -1132,7 +1137,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Zone.Deployable.DeployableIsDismissed(obj) => continent.tasks ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) - case InterstellarClusterService.ZonesResponse(zones) => + case ICS.ZonesResponse(zones) => zones.foreach { zone => val continentNumber = zone.Number val popBO = 0 @@ -1209,7 +1214,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con self ! NewPlayerLoaded(player) } else { zoneReload = true - cluster ! InterstellarClusterService.GetNearbySpawnPoint( + cluster ! ICS.GetNearbySpawnPoint( continent.Number, player, Seq(SpawnGroup.Facility, SpawnGroup.Tower), @@ -1218,7 +1223,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - case InterstellarClusterService.ZoneResponse(zone) => + case ICS.ZoneResponse(zone) => log.info(s"Zone ${zone.get.id} will now load") loadConfZone = true val oldZone = session.zone @@ -1622,55 +1627,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * You can't instant action to respond to some activity using a droppod! - * You can't. - * You just can't. + * Attach the player to a droppod vehicle and hurtle them through the stratosphere in some far off world. + * Perform all normal operation standardization (state cancels) as if any of form of zoning was being performed, + * then assemble the vehicle and work around some inconvenient setup requirements for vehicle gating. + * You can't instant action to respond to some activity using a droppod. * @param zone the destination zone - * @param hotspotPosition where is the hotspot that is being addressed - * @param spawnPosition the destination spawn position (may not be related to a literal `SpawnPoint` entity) + * @param spawnPosition the destination drop position */ - def YouCantInstantActionUsingDroppod(zone: Zone, hotspotPosition: Vector3, spawnPosition: Vector3): Unit = { + def LoadZoneLaunchDroppod(zone: Zone, spawnPosition: Vector3): Unit = { CancelZoningProcess() PlayerActionsToCancel() CancelAllProximityUnits() - //find a safe drop point - var targetBuildings = zone.Buildings.values - var whereToDroppod = spawnPosition.xy - while (targetBuildings.nonEmpty) { - (targetBuildings - .filter { building => - val radius = building.Definition.SOIRadius - Vector3.DistanceSquared(building.Position.xy, whereToDroppod) < radius * radius - }) match { - case Nil => - //no soi interference - targetBuildings = Nil - case List(building: Building) => - //blocked by a single soi; find space just outside of this soi and confirm no new overlap - val radius = Vector3(0, building.Definition.SOIRadius.toFloat + 5f, 0) - whereToDroppod = - building.Position.xy + Vector3.Rz(radius, math.abs(scala.util.Random.nextInt() % 360).toFloat) - case buildings => - //probably blocked by a facility and its tower (maximum overlap potential is 2?); find space outside of largest soi - val largestBuilding = buildings.maxBy(_.Definition.SOIRadius) - val radius = Vector3(0, largestBuilding.Definition.SOIRadius.toFloat + 5f, 0) - whereToDroppod = - largestBuilding.Position.xy + Vector3.Rz(radius, math.abs(scala.util.Random.nextInt() % 360).toFloat) - targetBuildings = buildings - } - } //droppod action val droppod = Vehicle(GlobalDefinitions.droppod) droppod.Faction = player.Faction - droppod.Position = whereToDroppod.xy + Vector3.z(1024) + droppod.Position = spawnPosition.xy + Vector3.z(1024) droppod.Orientation = Vector3.z(180) //you always seems to land looking south; don't know why - droppod.Seats(0).Occupant = player + droppod.Seats(0).mount(player) droppod.GUID = PlanetSideGUID(0) //droppod is not registered, we must jury-rig this droppod.Invalidate() //now, we must short-circuit the jury-rig interstellarFerry = Some(droppod) //leverage vehicle gating player.Position = droppod.Position + player.VehicleSeated = PlanetSideGUID(0) LoadZonePhysicalSpawnPoint(zone.id, droppod.Position, Vector3.Zero, 0 seconds) - /* Don't even think about it. */ } /** @@ -1850,7 +1829,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con reviveTimer.cancel() if (player.death_by == 0) { reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) { - cluster ! InterstellarClusterService.GetRandomSpawnPoint( + cluster ! ICS.GetRandomSpawnPoint( Zones.sanctuaryZoneNumber(player.Faction), player.Faction, Seq(SpawnGroup.Sanctuary), @@ -2330,6 +2309,26 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.SetEmpire(object_guid, empire) => sendResponse(SetEmpireMessage(object_guid, empire)) + case LocalResponse.SendResponse(pkt) => + sendResponse(pkt) + + case LocalResponse.ShuttleEvent(ev) => + val msg = OrbitalShuttleTimeMsg( + ev.u1, ev.u2, + ev.t1, ev.t2, ev.t3, + ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) } + ) + sendResponse(msg) + + case LocalResponse.ShuttleDock(pguid, sguid, slot) => + sendResponse(ObjectAttachMessage(pguid, sguid, slot)) + + case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) => + sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient)) + + case LocalResponse.ShuttleState(sguid, pos, orient, state) => + sendResponse(VehicleStateMessage(sguid, 0, pos, orient, None, Some(state), 0, 0, 15, false, false)) + case LocalResponse.ToggleTeleportSystem(router, system_plan) => ToggleTeleportSystem(router, system_plan) @@ -2350,7 +2349,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.RechargeVehicleWeapon(vehicle_guid, weapon_guid) => { if (tplayer_guid == guid) { continent.GUID(vehicle_guid) match { - case Some(vehicle: Mountable with MountedWeapons) => + case Some(vehicle: MountableWeapons) => vehicle.PassengerInSeat(player) match { case Some(seat_num: Int) => vehicle.WeaponControlledFromSeat(seat_num) match { @@ -2377,17 +2376,25 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def HandleMountMessages(tplayer: Player, reply: Mountable.Exchange): Unit = { reply match { - case Mountable.CanMount(obj: ImplantTerminalMech, seat_num) => + case Mountable.CanMount(obj: ImplantTerminalMech, seat_number, mount_point) => CancelZoningProcessWithDescriptiveReason("cancel_use") CancelAllProximityUnits() - MountingAction(tplayer, obj, seat_num) + MountingAction(tplayer, obj, seat_number) // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence - case Mountable.CanMount(obj: Vehicle, seat_num) => + case Mountable.CanMount(obj: Vehicle, seat_number, _) + if obj.Definition == GlobalDefinitions.orbital_shuttle => + CancelZoningProcessWithDescriptiveReason("cancel_mount") + CancelAllProximityUnits() + MountingAction(tplayer, obj, seat_number) + // the player will receive no messages consistently except the KeepAliveMessage echo + keepAliveFunc = KeepAlivePersistence + + case Mountable.CanMount(obj: Vehicle, seat_number, _) => CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID - log.info(s"MountVehicleMsg: ${player.Name}_guid mounts $obj_guid @ $seat_num") + log.info(s"MountVehicleMsg: ${player.Name}_guid mounts $obj_guid @ $seat_number") CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, 68, obj.Shields)) //shield health @@ -2398,29 +2405,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val capacitor = scala.math.ceil((obj.Capacitor.toFloat / obj.Definition.MaxCapacitor.toFloat) * 10).toInt sendResponse(PlanetsideAttributeMessage(obj_guid, 113, capacitor)) } - if (seat_num == 0) { + if (seat_number == 0) { if (obj.Definition == GlobalDefinitions.quadstealth) { //wraith cloak state matches the cloak state of the driver //phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks obj.Cloaked = tplayer.Cloaked } - } else if (obj.Seats(seat_num).ControlledWeapon.isEmpty) { + } else if (obj.WeaponControlledFromSeat(seat_number).isEmpty) { // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence } AccessContainer(obj) - UpdateWeaponAtSeatPosition(obj, seat_num) - MountingAction(tplayer, obj, seat_num) + UpdateWeaponAtSeatPosition(obj, seat_number) + MountingAction(tplayer, obj, seat_number) - case Mountable.CanMount(obj: FacilityTurret, seat_num) => + case Mountable.CanMount(obj: FacilityTurret, seat_number, mount_point) => CancelZoningProcessWithDescriptiveReason("cancel_mount") if (!obj.isUpgrading) { if (obj.Definition == GlobalDefinitions.vanu_sentry_turret) { obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction)) } sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) - UpdateWeaponAtSeatPosition(obj, seat_num) - MountingAction(tplayer, obj, seat_num) + UpdateWeaponAtSeatPosition(obj, seat_number) + MountingAction(tplayer, obj, seat_number) // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence } else { @@ -2429,25 +2436,67 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - case Mountable.CanMount(obj: PlanetSideGameObject with WeaponTurret, seat_num) => + case Mountable.CanMount(obj: PlanetSideGameObject with WeaponTurret, seat_number, mount_point) => CancelZoningProcessWithDescriptiveReason("cancel_mount") sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) - UpdateWeaponAtSeatPosition(obj, seat_num) - MountingAction(tplayer, obj, seat_num) + UpdateWeaponAtSeatPosition(obj, seat_number) + MountingAction(tplayer, obj, seat_number) // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence - case Mountable.CanMount(obj: Mountable, _) => + case Mountable.CanMount(obj: Mountable, _, _) => log.warn(s"MountVehicleMsg: $obj is some generic mountable object and nothing will happen") - case Mountable.CanDismount(obj: ImplantTerminalMech, seat_num) => + case Mountable.CanDismount(obj: ImplantTerminalMech, seat_num, _) => DismountAction(tplayer, obj, seat_num) - case Mountable.CanDismount(obj: Vehicle, seat_num) if obj.Definition == GlobalDefinitions.droppod => + case Mountable.CanDismount(obj: Vehicle, seat_num, mount_point) + if obj.Definition == GlobalDefinitions.orbital_shuttle => + val pguid = player.GUID + if (obj.MountedIn.nonEmpty) { + //dismount to hart lobby + val sguid = obj.GUID + val (pos, zang) = Vehicles.dismountShuttle(obj, mount_point) + tplayer.Position = pos + sendResponse(DelayedPathMountMsg(pguid, sguid, 60, true)) + continent.LocalEvents ! LocalServiceMessage( + continent.id, + LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, 0, 0, zang)) + ) + } + else { + //get ready for orbital drop + DismountAction(tplayer, obj, seat_num) + //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages + continent.VehicleEvents ! VehicleServiceMessage( + player.Name, + VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message + ) + //when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky + //the player will fall to the ground and is perfectly vulnerable in this state + //additionally, our player must exist in the current zone + //having no in-game avatar target will throw us out of the map screen when deploying and cause softlock + continent.VehicleEvents ! VehicleServiceMessage( + player.Name, + VehicleAction.SendResponse( + Service.defaultPlayerGUID, + PlayerStateShiftMessage(ShiftState(0, obj.Position, obj.Orientation.z, None)) //cower in the shuttle bay + ) + ) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, 9)) //conceal the player + ) + } + keepAliveFunc = NormalKeepAlive + + case Mountable.CanDismount(obj: Vehicle, seat_num, _) + if obj.Definition == GlobalDefinitions.droppod => UnaccessContainer(obj) DismountAction(tplayer, obj, seat_num) + obj.Actor ! Vehicle.Deconstruct() - case Mountable.CanDismount(obj: Vehicle, seat_num) => + case Mountable.CanDismount(obj: Vehicle, seat_num, _) => val player_guid: PlanetSideGUID = tplayer.GUID if (player_guid == player.GUID) { //disembarking self @@ -2461,26 +2510,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seat_num) => + case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seat_num, _) => DismountAction(tplayer, obj, seat_num) - case Mountable.CanDismount(obj: Mountable, _) => + case Mountable.CanDismount(obj: Mountable, _, _) => log.warn(s"DismountVehicleMsg: $obj is some generic mountable object and nothing will happen") - case Mountable.CanNotMount(obj: Vehicle, seat_num) => - log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seat_num, but was not allowed") - if (obj.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver)) { - sendResponse( - ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None) - ) + case Mountable.CanNotMount(obj: Vehicle, mount_point) => + log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's mount $mount_point, but was not allowed") + obj.GetSeatFromMountPoint(mount_point) match { + case Some(seatNum) if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) => + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None) + ) + case _ => } - case Mountable.CanNotMount(obj: Mountable, seat_num) => - log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seat_num, but was not allowed") + case Mountable.CanNotMount(obj: Mountable, mount_point) => + log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's mount $mount_point, but was not allowed") case Mountable.CanNotDismount(obj, seat_num) => log.warn( - s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's seat $seat_num, but was not allowed" + s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seat_num, but was not allowed" ) } } @@ -2657,7 +2708,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case VehicleResponse.KickPassenger(seat_num, wasKickedByDriver, vehicle_guid) => - // seat_num seems to be correct if passenger is kicked manually by driver, but always seems to return 4 if user is kicked by seat permissions + // seat_num seems to be correct if passenger is kicked manually by driver, but always seems to return 4 if user is kicked by mount permissions sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) player.VehicleSeated = None if (tplayer_guid == guid) { @@ -3052,9 +3103,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse( DroppodFreefallingMessage( vehicle.GUID, - vehicle.Position + Vector3.z(50), - Vector3.z(-999), - vehicle.Position + Vector3.z(25), + vehicle.Position, + Vector3.z(value = -999), + vehicle.Position + Vector3(-20, 1.156f, -50), Vector3(0, 70.3125f, 90), Vector3(0, 0, 90) ) @@ -3328,7 +3379,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con .asInstanceOf[Mountable] .Seats .values - .map(_.Occupant) + .map(_.occupant) .collect { case Some(occupant) => if (occupant.isAlive) { @@ -3444,10 +3495,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) //occupants other than driver vehicle.Seats - .filter({ case (index, seat) => seat.isOccupied && live.contains(seat.Occupant.get) && index > 0 }) + .filter({ case (index, seat) => seat.isOccupied && live.contains(seat.occupant.get) && index > 0 }) .foreach({ case (index, seat) => - val targetPlayer = seat.Occupant.get + val targetPlayer = seat.occupant.get val targetDefiniton = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3471,11 +3522,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vehicle.Seats .filter({ case (index, seat) => - seat.isOccupied && !seat.Occupant.contains(player) && live.contains(seat.Occupant.get) && index > 0 + seat.isOccupied && !seat.occupant.contains(player) && live.contains(seat.occupant.get) && index > 0 }) .foreach({ case (index, seat) => - val targetPlayer = seat.Occupant.get + val targetPlayer = seat.occupant.get val targetDefinition = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3486,7 +3537,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) }) - //since we would have only subscribed recently, we need to reload seat access states + //since we would have only subscribed recently, we need to reload mount access states (0 to 3).foreach { group => sendResponse(PlanetsideAttributeMessage(vguid, group + 10, vehicle.PermissionGroup(group).get.id)) } @@ -3509,15 +3560,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //cargo occupants (including our own vehicle as cargo) allActiveVehicles.collect { case vehicle if vehicle.CargoHolds.nonEmpty => - vehicle.CargoHolds.collect({ - case (index, hold: Cargo) if hold.isOccupied => { + vehicle.CargoHolds.collect { + case (_index, hold: Cargo) if hold.isOccupied => CargoBehavior.CargoMountBehaviorForAll( vehicle, - hold.Occupant.get, - index + hold.occupant.get, + _index ) //CargoMountBehaviorForUs can fail to attach the cargo vehicle on some clients - } - }) + } } //special deploy states val deployedVehicles = allActiveVehicles.filter(_.DeploymentState == DriveState.Deployed) @@ -3542,6 +3592,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, false, Vector3.Zero)) ToggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } + val name = avatar.name + serviceManager.ask(Lookup("hart"))(Timeout(2 seconds)) + .onComplete { + case Success(LookupResult("hart", ref)) => + ref ! HartTimer.Update(continentId, name) + case _ => + } //implant terminals continent.map.terminalToInterface.foreach({ @@ -3560,11 +3617,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) case _ => ; } - //seat terminal occupants + //mount terminal occupants continent.GUID(terminal_guid) match { case Some(obj: Mountable) => - obj.Seats(0).Occupant match { - case Some(targetPlayer) => + obj.Seats(0).occupant match { + case Some(targetPlayer: Player) => val targetDefinition = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3574,7 +3631,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con targetDefinition.Packet.ConstructorData(targetPlayer).get ) ) - case None => ; + case _ => ; } case _ => ; } @@ -3604,9 +3661,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } //reserved ammunition? //TODO need to register if it exists - //seat turret occupant - turret.Seats(0).Occupant match { - case Some(targetPlayer) => + //mount turret occupant + turret.Seats(0).occupant match { + case Some(targetPlayer: Player) => val targetDefinition = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3616,7 +3673,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con targetDefinition.Packet.ConstructorData(targetPlayer).get ) ) - case None => ; + case _ => ; } } continent.VehicleEvents ! VehicleServiceMessage( @@ -3650,6 +3707,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (isMovingPlus) { CancelZoningProcessWithDescriptiveReason("cancel_motion") } +// if (is_crouching && !player.Crouching) { +// //dev stuff goes here +// } player.Position = pos player.Velocity = vel player.Orientation = Vector3(player.Orientation.x, pitch, yaw) @@ -3780,7 +3840,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con turnCounterFunc(player.GUID) val seat = obj.Seats(0) player.Position = pos //convenient - if (seat.ControlledWeapon.isEmpty) { + if (obj.WeaponControlledFromSeat(0).isEmpty) { player.Orientation = Vector3.z(ang.z) //convenient } obj.Position = pos @@ -3792,12 +3852,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.Velocity = Some(Vector3.Zero) } if (obj.Definition.CanFly) { - obj.Flying = flying.nonEmpty //usually Some(7) + obj.Flying = flying //usually Some(7) } obj.Cloaked = obj.Definition.CanCloak && is_cloaked } else { obj.Velocity = None - obj.Flying = false + obj.Flying = None } continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -3808,7 +3868,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.Position, ang, obj.Velocity, - if (obj.Flying) { + if (obj.isFlying) { flying } else { None @@ -3876,7 +3936,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.info(s"SpawnRequestMessage: $msg") if (deadState != DeadState.RespawnTime) { deadState = DeadState.RespawnTime - cluster ! InterstellarClusterService.GetNearbySpawnPoint( + cluster ! ICS.GetNearbySpawnPoint( spawnGroup match { case SpawnGroup.Sanctuary => Zones.sanctuaryZoneNumber(player.Faction) @@ -4246,14 +4306,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(vehicle: Vehicle) => /* line 1a: player is admin (and overrules other access requirements) */ /* line 1b: vehicle and player (as the owner) acknowledge each other */ - /* line 1c: vehicle is the same faction as player and either the owner is absent or the vehicle is destroyed */ + /* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */ /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */ if ( (session.account.gm || (player.avatar.vehicle.contains(object_guid) && vehicle.Owner.contains(player.GUID)) || - (player.Faction == vehicle.Faction && ((vehicle.Owner.isEmpty || continent - .GUID(vehicle.Owner.get) - .isEmpty) || vehicle.Destroyed))) && + (player.Faction == vehicle.Faction && + (vehicle.Definition.CanBeOwned.nonEmpty && + (vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))) && (vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied)) ) { vehicle.Actor ! Vehicle.Deconstruct() @@ -5333,7 +5393,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con true })) => deadState = DeadState.RespawnTime - cluster ! InterstellarClusterService.GetSpawnPoint( + cluster ! ICS.GetSpawnPoint( destinationZoneGuid.guid, player, destinationBuildingGuid, @@ -5355,14 +5415,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.info("MountVehicleMsg: " + msg) ValidObject(mountable_guid) match { case Some(obj: Mountable) => - obj.GetSeatFromMountPoint(entry_point) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryMount(player, seat_num) - case None => - log.warn( - s"MountVehicleMsg: attempted to board mountable $mountable_guid's seat $entry_point, but no seat exists there" - ) - } + obj.Actor ! Mountable.TryMount(player, entry_point) case None | Some(_) => log.warn(s"MountVehicleMsg: not a mountable thing") } @@ -5375,10 +5428,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.warn(s"$msg; some vehicle might not know that a player is no longer sitting in it") } if (player.GUID == player_guid) { - //normally disembarking from a seat + //normally disembarking from a mount (interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { case out @ Some(obj: Vehicle) => - if (obj.MountedIn.isEmpty) out else None + continent.GUID(obj.MountedIn) match { + case Some(_: Vehicle) => None //cargo vehicle + case _ => out //arrangement "may" be permissible + } case out @ Some(_: Mountable) => out case _ => @@ -5405,7 +5461,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. //todo: kick cargo passengers out. To be added after PR #216 is merged obj match { - case v: Vehicle if bailType == BailType.Bailed && seat_num == 0 && v.Flying => + case v: Vehicle + if bailType == BailType.Bailed && + v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) && + v.isFlying => v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction case _ => ; } @@ -5419,7 +5478,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con dismountWarning(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}") } } else { - //kicking someone else out of a seat; need to own that seat/mountable + //kicking someone else out of a mount; need to own that mount/mountable player.avatar.vehicle match { case Some(obj_guid) => ((ValidObject(obj_guid), ValidObject(player_guid)) match { @@ -5521,41 +5580,33 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) //kick players who should not be seated in the vehicle due to permission changes if (allow == VehicleLockState.Locked) { //TODO only important permission atm - vehicle.Definition.MountPoints.values - .foreach(mountpoint_num => { - vehicle.Seat(mountpoint_num) match { - case Some(seat) => - seat.Occupant match { - case Some(tplayer) => - if ( - vehicle.SeatPermissionGroup(mountpoint_num).contains(group) && tplayer != player - ) { //can not kick self - seat.Occupant = None - tplayer.VehicleSeated = None - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.KickPassenger(tplayer.GUID, 4, false, object_guid) - ) - } - case None => ; // No player seated - } - case None => ; // Not a seat mounting point - } - vehicle.CargoHold(mountpoint_num) match { - case Some(cargo) => - cargo.Occupant match { - case Some(vehicle) => - if (vehicle.SeatPermissionGroup(mountpoint_num).contains(group)) { - //todo: this probably doesn't work for passengers within the cargo vehicle - // Instruct client to start bail dismount procedure - self ! DismountVehicleCargoMsg(player.GUID, vehicle.GUID, true, false, false) - } - case None => ; // No vehicle in cargo - } - case None => ; // Not a cargo mounting point - } - - }) + vehicle.Seats.foreach { case (seatIndex, seat) => + seat.occupant match { + case Some(tplayer : Player) => + if ( + vehicle.SeatPermissionGroup(seatIndex).contains(group) && tplayer != player + ) { //can not kick self + seat.unmount(tplayer) + tplayer.VehicleSeated = None + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.KickPassenger(tplayer.GUID, 4, false, object_guid) + ) + } + case _ => ; // No player seated + } + } + vehicle.CargoHolds.foreach { case (cargoIndex, hold) => + hold.occupant match { + case Some(cargo) => + if (vehicle.SeatPermissionGroup(cargoIndex).contains(group)) { + //todo: this probably doesn't work for passengers within the cargo vehicle + // Instruct client to start bail dismount procedure + self ! DismountVehicleCargoMsg(player.GUID, cargo.GUID, true, false, false) + } + case None => ; // No vehicle in cargo + } + } } case None => ; } @@ -5624,6 +5675,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con }) sendResponse(TargetingInfoMessage(targetInfo)) + case msg @ DroppodLaunchRequestMessage(info, _) => + //log.info(s"Droppod request: $msg") + cluster ! ICS.DroppodLaunchRequest( + info.zone_number, + info.xypos, + player.Faction, + self.toTyped[ICS.DroppodLaunchExchange] + ) + case msg @ ActionCancelMessage(u1, u2, u3) => log.info("Cancelled: " + msg) progressBarUpdate.cancel() @@ -6169,7 +6229,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Along with any discovered item, a containing object such that the statement:
* `container.Find(object) = Some(slot)`
* ... will return a proper result. - * For a seat controlled weapon, the vehicle is returned. + * For a mount controlled weapon, the vehicle is returned. * For the player's hand, the player is returned. * @return a `Tuple` of the returned values; * the first value is a `Container` object; @@ -6179,7 +6239,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.VehicleSeated match { case Some(vehicle_guid) => //weapon is vehicle turret? continent.GUID(vehicle_guid) match { - case Some(vehicle: Mountable with MountedWeapons with Container) => + case Some(vehicle: Mountable with MountableWeapons with Container) => vehicle.PassengerInSeat(player) match { case Some(seat_num) => (Some(vehicle), vehicle.WeaponControlledFromSeat(seat_num)) @@ -6663,7 +6723,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @see `Door` * @see `GenericObjectStateMsg` * @see `Hackable` - * @see `HackCaptureTerminal` * @see `HackObject` * @see `PlanetsideAttributeMessage` * @see `ResourceSilo` @@ -6831,7 +6890,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con *
* If that player is in a vehicle, it will construct that vehicle. * If the player is the driver of the vehicle, - * they must temporarily be removed from the driver seat in order for the vehicle to be constructed properly. + * they must temporarily be removed from the driver mount in order for the vehicle to be constructed properly. * These two previous statements operate through similar though distinct mechanisms and imply different conditions. * In reality, they produce the same output but enforce different relationships between the components. * The vehicle without a rendered player will always be created if that vehicle exists. @@ -6851,7 +6910,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //if the vehicle is the cargo of another vehicle in this zone val carrierInfo = continent.GUID(vehicle.MountedIn) match { case Some(carrier: Vehicle) => - (Some(carrier), carrier.CargoHolds.find({ case (index, hold) => hold.Occupant.contains(vehicle) })) + (Some(carrier), carrier.CargoHolds.find({ case (index, hold) => hold.occupant.contains(vehicle) })) case _ => (None, None) } @@ -6866,13 +6925,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.Transport ! Zone.Vehicle.Spawn(vehicle) //as the driver, we must temporarily exclude ourselves from being in the vehicle during its creation val seat = vehicle.Seats(0) - seat.Occupant = None + seat.unmount(player) + player.VehicleSeated = None val data = vdef.Packet.ConstructorData(vehicle).get sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, data)) - seat.Occupant = player + seat.mount(player) Vehicles.Own(vehicle, player) vehicle.CargoHolds.values - .collect { case hold if hold.isOccupied => hold.Occupant.get } + .collect { case hold if hold.isOccupied => hold.occupant.get } .foreach { _.MountedIn = vguid } continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -6938,12 +6998,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * If the player is mounted in some entity, find that entity and get the seat index number at which the player is sat. + * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat. * The priority of object confirmation is `direct` then `occupant.VehicleSeated`. * Once an object is found, the remainder are ignored. * @param direct a game object in which the player may be sat * @param occupant the player who is sat and may have specified the game object in which mounted - * @return a tuple consisting of a vehicle reference and a seat index + * @return a tuple consisting of a vehicle reference and a mount index * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ @@ -6965,7 +7025,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat.
+ * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
*
* For special purposes involved in zone transfers, * where the vehicle may or may not exist in either of the zones (yet), @@ -6974,7 +7034,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * to avoid inspecting the wrong vehicle and failing simple vehicle checks where this function may be employed. * @see `GetMountableAndSeat` * @see `interstellarFerry` - * @return a tuple consisting of a vehicle reference and a seat index + * @return a tuple consisting of a vehicle reference and a mount index * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ @@ -6985,9 +7045,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat. + * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat. * @see `GetMountableAndSeat` - * @return a tuple consisting of a vehicle reference and a seat index + * @return a tuple consisting of a vehicle reference and a mount index * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ @@ -6998,7 +7058,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * Create an avatar character so that avatar's player is mounted in a vehicle's seat. + * Create an avatar character so that avatar's player is mounted in a vehicle's mount. * A part of the process of spawning the player into the game world.
*
* This is a very specific configuration of the player character that is not visited very often. @@ -7011,9 +7071,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * to avoid damaging the critical setup of this function. * @see `AccessContainer` * @see `UpdateWeaponAtSeatPosition` - * @param tplayer the player avatar seated in the vehicle's seat + * @param tplayer the player avatar seated in the vehicle's mount * @param vehicle the vehicle the player is riding - * @param seat the seat index + * @param seat the mount index */ def AvatarCreateInVehicle(tplayer: Player, vehicle: Vehicle, seat: Int): Unit = { val pdef = tplayer.avatar.definition @@ -7023,7 +7083,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val pdata = pdef.Packet.DetailedConstructorData(tplayer).get tplayer.VehicleSeated = vguid sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata)) - if (seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) { + if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) { sendResponse(ObjectAttachMessage(vguid, pguid, seat)) AccessContainer(vehicle) UpdateWeaponAtSeatPosition(vehicle, seat) @@ -7058,7 +7118,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con *
* If that player is in a vehicle, it will construct that vehicle. * If the player is the driver of the vehicle, - * they must temporarily be removed from the driver seat in order for the vehicle to be constructed properly. + * they must temporarily be removed from the driver mount in order for the vehicle to be constructed properly. * These two previous statements operate through similar though distinct mechanisms and imply different conditions. * In reality, they produce the same output but enforce different relationships between the components. * The vehicle without a rendered player will always be created if that vehicle exists.
@@ -7079,10 +7139,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val vguid = vehicle.GUID if (seat == 0) { val seat = vehicle.Seats(0) - seat.Occupant = None + seat.unmount(player) val vdata = vdef.Packet.ConstructorData(vehicle).get sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata)) - seat.Occupant = player + seat.mount(player) } else { val vdata = vdef.Packet.ConstructorData(vehicle).get sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata)) @@ -7097,7 +7157,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.VehicleSeated = vguid sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata)) //log.info(s"AvatarRejoin: $vguid -> $vdata") - if (seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) { + if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) { sendResponse(ObjectAttachMessage(vguid, pguid, seat)) AccessContainer(vehicle) UpdateWeaponAtSeatPosition(vehicle, seat) @@ -7290,14 +7350,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.GUID(player.VehicleSeated) match { case Some(obj: Vehicle) if !obj.Destroyed => - cluster ! InterstellarClusterService.GetRandomSpawnPoint( + cluster ! ICS.GetRandomSpawnPoint( Zones.sanctuaryZoneNumber(player.Faction), player.Faction, Seq(SpawnGroup.WarpGate), context.self ) case _ => - cluster ! InterstellarClusterService.GetRandomSpawnPoint( + cluster ! ICS.GetRandomSpawnPoint( Zones.sanctuaryZoneNumber(player.Faction), player.Faction, Seq(SpawnGroup.Sanctuary), @@ -7703,7 +7763,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Common activities/procedure when a player mounts a valid object. * @param tplayer the player * @param obj the mountable object - * @param seatNum the seat into which the player is mounting + * @param seatNum the mount into which the player is mounting */ def MountingAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { val player_guid: PlanetSideGUID = tplayer.GUID @@ -7723,13 +7783,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Common activities/procedure when a player dismounts a valid object. * @param tplayer the player * @param obj the mountable object - * @param seatNum the seat out of which which the player is disembarking + * @param seatNum the mount out of which which the player is disembarking */ def DismountAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { val player_guid: PlanetSideGUID = tplayer.GUID log.info(s"DismountVehicleMsg: ${tplayer.Name} dismounts $obj from $seatNum") keepAliveFunc = NormalKeepAlive - sendResponse(DismountVehicleMsg(player_guid, BailType.Normal, false)) + sendResponse(DismountVehicleMsg(player_guid, BailType.Normal, wasKickedByDriver = false)) continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.DismountVehicle(player_guid, BailType.Normal, false) @@ -8417,15 +8477,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con session = session.copy(player = targetPlayer) taskThenZoneChange( GUIDTask.UnregisterObjectTask(original.avatar.locker)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } else if (player.HasGUID) { taskThenZoneChange( GUIDTask.UnregisterAvatar(original)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } else { - cluster ! InterstellarClusterService.FindZone(_.id == zoneId, context.self) + cluster ! ICS.FindZone(_.id == zoneId, context.self) } } @@ -8494,11 +8554,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) manifest.cargo.foreach { case ("MISSING_DRIVER", index) => - val cargo = vehicle.CargoHolds(index).Occupant.get + val cargo = vehicle.CargoHolds(index).occupant.get log.error(s"LoadZoneInVehicleAsDriver: eject cargo in hold $index; vehicle missing driver") CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true) case (name, index) => - val cargo = vehicle.CargoHolds(index).Occupant.get + val cargo = vehicle.CargoHolds(index).occupant.get continent.VehicleEvents ! VehicleServiceMessage( name, VehicleAction.TransferPassengerChannel(pguid, s"${cargo.Actor}", toChannel, cargo, topLevel) @@ -8519,7 +8579,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.Continent = zoneId //forward-set the continent id to perform a test taskThenZoneChange( GUIDTask.UnregisterAvatar(player)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } else { UnaccessContainer(vehicle) @@ -8543,7 +8603,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.Transport ! Zone.Vehicle.Despawn(vehicle) taskThenZoneChange( UnregisterDrivenVehicle(vehicle, player), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } } @@ -8587,7 +8647,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con taskThenZoneChange( GUIDTask.UnregisterAvatar(player)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } } @@ -8614,7 +8674,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vehicle.CargoHolds.values .collect { case hold if hold.isOccupied => - val cargo = hold.Occupant.get + val cargo = hold.occupant.get cargo.Continent = toZoneId //point to the cargo vehicle to instigate cargo vehicle driver transportation galaxyService ! GalaxyServiceMessage( @@ -8630,7 +8690,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** Before changing zones, perform the following task (which can be a nesting of subtasks). */ def taskThenZoneChange( task: TaskResolver.GiveTask, - zoneMessage: InterstellarClusterService.FindZone + zoneMessage: ICS.FindZone ): Unit = { continent.tasks ! TaskResolver.GiveTask( new Task() { @@ -8845,14 +8905,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * From a seat, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines. + * From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines. * @param objWithSeat the object that owns seats (and weaponry) - * @param seatNum the seat + * @param seatNum the mount */ - def UpdateWeaponAtSeatPosition(objWithSeat: MountedWeapons, seatNum: Int): Unit = { + def UpdateWeaponAtSeatPosition(objWithSeat: MountableWeapons, seatNum: Int): Unit = { objWithSeat.WeaponControlledFromSeat(seatNum) match { case Some(weapon: Tool) => - //update mounted weapon belonging to seat + //update mounted weapon belonging to mount weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically val magazine = slot.Box @@ -9094,9 +9154,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con .flatMap { case Some(obj: Vehicle) if !obj.Cloaked => //TODO hint: vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.ProjectileAutoLockAwareness(mode)) - obj.Seats.values.collect { case seat if seat.isOccupied => seat.Occupant.get.Name } + obj.Seats.values.flatMap { case seat if seat.isOccupied => seat.occupants.map(_.Name) } case Some(obj: Mountable) => - obj.Seats.values.collect { case seat if seat.isOccupied => seat.Occupant.get.Name } + obj.Seats.values.flatMap { case seat if seat.isOccupied => seat.occupants.map(_.Name) } case Some(obj: Player) if obj.ExoSuit == ExoSuitType.MAX => Seq(obj.Name) case _ => @@ -9216,15 +9276,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Until new upstream messages that pass some tests against their data start being reported, * the counter does not accumulate properly.
*
- * In the case that the transitioning player is seated in a vehicle seat + * In the case that the transitioning player is seated in a vehicle mount * that is not the driver and does not have a mounted weapon under its control, * no obvious feedback will be provided by the client. * For example, when as infantry, a `PlayerStateMessageUpstream` packet is dispatched by the client. - * For example, when in the driver seat, a `VehicleStateMessage` is dispatched by the client. + * For example, when in the driver mount, a `VehicleStateMessage` is dispatched by the client. * In the given case, the only packet that indicates the player is seated is a `KeepAliveMessage`. * Detection of this `KeepALiveMessage`, for the purpose of transitioning logic, * can not be instantaneous to the zoning process or other checks for proper zoning conditions that will be disrupted. - * To avoid complications, the player in such a seat is initially spawned as infantry on their own client, + * To avoid complications, the player in such a mount is initially spawned as infantry on their own client, * realizes the state transition confirmation for infantry (turn counter), * and is forced to transition into being seated, * and only at that time will begin registering `KeepAliveMessage` to mark the end of their interim period. @@ -9265,7 +9325,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * The atypical response to receiving a `KeepAliveMessage` packet from the client.
*
* `KeepAliveMessage` packets are the primary vehicle for persistence due to client reporting - * in the case where the player's avatar is riding in a vehicle in a seat with no vehicle. + * in the case where the player's avatar is riding in a vehicle in a mount with no vehicle. * @see `KeepAliveMessage` * @see `keepAliveFunc` * @see `turnCounterFunc` @@ -9285,7 +9345,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con GetMountableAndSeat(None, tplayer, continent) match { case (Some(obj), Some(seatNum)) => tplayer.VehicleSeated = None - obj.Seats(seatNum).Occupant = None + obj.Seats(seatNum).unmount(tplayer) continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.KickPassenger(tplayer.GUID, seatNum, false, obj.GUID) diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index cb0ce2bd9..0c2c5cfaf 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -1,6 +1,5 @@ package net.psforever.actors.zone -import akka.actor.Actor import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} @@ -9,10 +8,8 @@ import net.psforever.actors.commands.NtuCommand import net.psforever.objects.NtuContainer import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} -import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} -import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretControl} import net.psforever.objects.zones.Zone import net.psforever.persistence import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 6b9eabb57..fc3c00a35 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -236,9 +236,10 @@ object ExplosiveDeployableControl { def detectTarget(g1: Geometry3D, up: Vector3)(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float) : Boolean = { val g2 = obj2.Definition.Geometry(obj2) val dir = g2.center.asVector3 - g1.center.asVector3 - val scalar = Vector3.ScalarProjection(dir, up) + //val scalar = Vector3.ScalarProjection(dir, up) val point1 = g1.pointOnOutside(dir).asVector3 val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3 + val scalar = Vector3.ScalarProjection(point2 - point1, up) (scalar >= 0 || Vector3.MagnitudeSquared(up * scalar) < 0.35f) && math.min( Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3), diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index eea381a8c..7a9bcc38e 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -14,16 +14,17 @@ import net.psforever.objects.serverobject.doors.DoorDefinition import net.psforever.objects.serverobject.generator.GeneratorDefinition import net.psforever.objects.serverobject.locks.IFFLockDefinition import net.psforever.objects.serverobject.mblocker.LockerDefinition +import net.psforever.objects.serverobject.mount._ import net.psforever.objects.serverobject.pad.VehicleSpawnPadDefinition import net.psforever.objects.serverobject.painbox.PainboxDefinition import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.tube.SpawnTubeDefinition import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition -import net.psforever.objects.serverobject.structures.{AutoRepairStats, BuildingDefinition, WarpGateDefinition} +import net.psforever.objects.serverobject.structures.{AmenityDefinition, AutoRepairStats, BuildingDefinition, WarpGateDefinition} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition} import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade} -import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, SeatArmorRestriction, UtilityType} +import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType} import net.psforever.objects.vital.base.DamageType import net.psforever.objects.vital.damage._ import net.psforever.objects.vital.etc.ExplodingRadialDegrade @@ -938,6 +939,8 @@ object GlobalDefinitions { val phantasm = VehicleDefinition(ObjectClass.phantasm) val droppod = VehicleDefinition(ObjectClass.droppod) + + val orbital_shuttle = VehicleDefinition(ObjectClass.orbital_shuttle) init_vehicles() /* @@ -1056,6 +1059,8 @@ object GlobalDefinitions { val door = new DoorDefinition + val gr_door_mb_orb = new DoorDefinition + val resource_silo = new ResourceSiloDefinition val capture_terminal = new CaptureTerminalDefinition(158) // Base CC @@ -1093,6 +1098,9 @@ object GlobalDefinitions { val gen_control = new GeneratorTerminalDefinition(349) val generator = new GeneratorDefinition(351) + + val obbasemesh = new AmenityDefinition(598) { } + initMiscellaneous() /* @@ -5611,18 +5619,24 @@ object GlobalDefinitions { val apcForm = GeometryForm.representByCylinder(radius = 4.6211f, height = 3.90626f) _ //TODO hexahedron val liberatorForm = GeometryForm.representByCylinder(radius = 3.74615f, height = 2.51563f) _ + val bailableSeat = new SeatDefinition() { + bailable = true + } + val maxOnlySeat = new SeatDefinition() { + restriction = MaxOnly + } + fury.Name = "fury" fury.MaxHealth = 650 fury.Damageable = true fury.Repairable = true fury.RepairIfDestroyed = false fury.MaxShields = 130 - fury.Seats += 0 -> new SeatDefinition() - fury.Seats(0).Bailable = true - fury.Seats(0).ControlledWeapon = 1 + fury.Seats += 0 -> bailableSeat + fury.controlledWeapons += 0 -> 1 fury.Weapons += 1 -> fury_weapon_systema - fury.MountPoints += 1 -> 0 - fury.MountPoints += 2 -> 0 + fury.MountPoints += 1 -> MountInfo(0) + fury.MountPoints += 2 -> MountInfo(0) fury.TrunkSize = InventoryTile.Tile1111 fury.TrunkOffset = 30 fury.TrunkLocation = Vector3(-1.71f, 0f, 0f) @@ -5649,12 +5663,11 @@ object GlobalDefinitions { quadassault.Repairable = true quadassault.RepairIfDestroyed = false quadassault.MaxShields = 130 - quadassault.Seats += 0 -> new SeatDefinition() - quadassault.Seats(0).Bailable = true - quadassault.Seats(0).ControlledWeapon = 1 + quadassault.Seats += 0 -> bailableSeat + quadassault.controlledWeapons += 0 -> 1 quadassault.Weapons += 1 -> quadassault_weapon_system - quadassault.MountPoints += 1 -> 0 - quadassault.MountPoints += 2 -> 0 + quadassault.MountPoints += 1 -> MountInfo(0) + quadassault.MountPoints += 2 -> MountInfo(0) quadassault.TrunkSize = InventoryTile.Tile1111 quadassault.TrunkOffset = 30 quadassault.TrunkLocation = Vector3(-1.71f, 0f, 0f) @@ -5682,11 +5695,10 @@ object GlobalDefinitions { quadstealth.RepairIfDestroyed = false quadstealth.MaxShields = 130 quadstealth.CanCloak = true - quadstealth.Seats += 0 -> new SeatDefinition() - quadstealth.Seats(0).Bailable = true + quadstealth.Seats += 0 -> bailableSeat quadstealth.CanCloak = true - quadstealth.MountPoints += 1 -> 0 - quadstealth.MountPoints += 2 -> 0 + quadstealth.MountPoints += 1 -> MountInfo(0) + quadstealth.MountPoints += 2 -> MountInfo(0) quadstealth.TrunkSize = InventoryTile.Tile1111 quadstealth.TrunkOffset = 30 quadstealth.TrunkLocation = Vector3(-1.71f, 0f, 0f) @@ -5713,14 +5725,12 @@ object GlobalDefinitions { two_man_assault_buggy.Repairable = true two_man_assault_buggy.RepairIfDestroyed = false two_man_assault_buggy.MaxShields = 250 - two_man_assault_buggy.Seats += 0 -> new SeatDefinition() - two_man_assault_buggy.Seats(0).Bailable = true - two_man_assault_buggy.Seats += 1 -> new SeatDefinition() - two_man_assault_buggy.Seats(1).Bailable = true - two_man_assault_buggy.Seats(1).ControlledWeapon = 2 + two_man_assault_buggy.Seats += 0 -> bailableSeat + two_man_assault_buggy.Seats += 1 -> bailableSeat + two_man_assault_buggy.controlledWeapons += 1 -> 2 two_man_assault_buggy.Weapons += 2 -> chaingun_p - two_man_assault_buggy.MountPoints += 1 -> 0 - two_man_assault_buggy.MountPoints += 2 -> 1 + two_man_assault_buggy.MountPoints += 1 -> MountInfo(0) + two_man_assault_buggy.MountPoints += 2 -> MountInfo(1) two_man_assault_buggy.TrunkSize = InventoryTile.Tile1511 two_man_assault_buggy.TrunkOffset = 30 two_man_assault_buggy.TrunkLocation = Vector3(-2.5f, 0f, 0f) @@ -5747,15 +5757,13 @@ object GlobalDefinitions { skyguard.Repairable = true skyguard.RepairIfDestroyed = false skyguard.MaxShields = 200 - skyguard.Seats += 0 -> new SeatDefinition() - skyguard.Seats(0).Bailable = true - skyguard.Seats += 1 -> new SeatDefinition() - skyguard.Seats(1).Bailable = true - skyguard.Seats(1).ControlledWeapon = 2 + skyguard.Seats += 0 -> bailableSeat + skyguard.Seats += 1 -> bailableSeat + skyguard.controlledWeapons += 1 -> 2 skyguard.Weapons += 2 -> skyguard_weapon_system - skyguard.MountPoints += 1 -> 0 - skyguard.MountPoints += 2 -> 0 - skyguard.MountPoints += 3 -> 1 + skyguard.MountPoints += 1 -> MountInfo(0) + skyguard.MountPoints += 2 -> MountInfo(0) + skyguard.MountPoints += 3 -> MountInfo(1) skyguard.TrunkSize = InventoryTile.Tile1511 skyguard.TrunkOffset = 30 skyguard.TrunkLocation = Vector3(2.5f, 0f, 0f) @@ -5782,19 +5790,16 @@ object GlobalDefinitions { threemanheavybuggy.Repairable = true threemanheavybuggy.RepairIfDestroyed = false threemanheavybuggy.MaxShields = 340 - threemanheavybuggy.Seats += 0 -> new SeatDefinition() - threemanheavybuggy.Seats(0).Bailable = true - threemanheavybuggy.Seats += 1 -> new SeatDefinition() - threemanheavybuggy.Seats(1).Bailable = true - threemanheavybuggy.Seats(1).ControlledWeapon = 3 - threemanheavybuggy.Seats += 2 -> new SeatDefinition() - threemanheavybuggy.Seats(2).Bailable = true - threemanheavybuggy.Seats(2).ControlledWeapon = 4 + threemanheavybuggy.Seats += 0 -> bailableSeat + threemanheavybuggy.Seats += 1 -> bailableSeat + threemanheavybuggy.Seats += 2 -> bailableSeat + threemanheavybuggy.controlledWeapons += 1 -> 3 + threemanheavybuggy.controlledWeapons += 2 -> 4 threemanheavybuggy.Weapons += 3 -> chaingun_p threemanheavybuggy.Weapons += 4 -> grenade_launcher_marauder - threemanheavybuggy.MountPoints += 1 -> 0 - threemanheavybuggy.MountPoints += 2 -> 1 - threemanheavybuggy.MountPoints += 3 -> 2 + threemanheavybuggy.MountPoints += 1 -> MountInfo(0) + threemanheavybuggy.MountPoints += 2 -> MountInfo(1) + threemanheavybuggy.MountPoints += 3 -> MountInfo(2) threemanheavybuggy.TrunkSize = InventoryTile.Tile1511 threemanheavybuggy.TrunkOffset = 30 threemanheavybuggy.TrunkLocation = Vector3(3.01f, 0f, 0f) @@ -5822,14 +5827,12 @@ object GlobalDefinitions { twomanheavybuggy.Repairable = true twomanheavybuggy.RepairIfDestroyed = false twomanheavybuggy.MaxShields = 360 - twomanheavybuggy.Seats += 0 -> new SeatDefinition() - twomanheavybuggy.Seats(0).Bailable = true - twomanheavybuggy.Seats += 1 -> new SeatDefinition() - twomanheavybuggy.Seats(1).Bailable = true - twomanheavybuggy.Seats(1).ControlledWeapon = 2 + twomanheavybuggy.Seats += 0 -> bailableSeat + twomanheavybuggy.Seats += 1 -> bailableSeat + twomanheavybuggy.controlledWeapons += 1 -> 2 twomanheavybuggy.Weapons += 2 -> advanced_missile_launcher_t - twomanheavybuggy.MountPoints += 1 -> 0 - twomanheavybuggy.MountPoints += 2 -> 1 + twomanheavybuggy.MountPoints += 1 -> MountInfo(0) + twomanheavybuggy.MountPoints += 2 -> MountInfo(1) twomanheavybuggy.TrunkSize = InventoryTile.Tile1511 twomanheavybuggy.TrunkOffset = 30 twomanheavybuggy.TrunkLocation = Vector3(-0.23f, -2.05f, 0f) @@ -5857,14 +5860,12 @@ object GlobalDefinitions { twomanhoverbuggy.Repairable = true twomanhoverbuggy.RepairIfDestroyed = false twomanhoverbuggy.MaxShields = 320 - twomanhoverbuggy.Seats += 0 -> new SeatDefinition() - twomanhoverbuggy.Seats(0).Bailable = true - twomanhoverbuggy.Seats += 1 -> new SeatDefinition() - twomanhoverbuggy.Seats(1).Bailable = true - twomanhoverbuggy.Seats(1).ControlledWeapon = 2 + twomanhoverbuggy.Seats += 0 -> bailableSeat + twomanhoverbuggy.Seats += 1 -> bailableSeat + twomanhoverbuggy.controlledWeapons += 1 -> 2 twomanhoverbuggy.Weapons += 2 -> flux_cannon_thresher - twomanhoverbuggy.MountPoints += 1 -> 0 - twomanhoverbuggy.MountPoints += 2 -> 1 + twomanhoverbuggy.MountPoints += 1 -> MountInfo(0) + twomanhoverbuggy.MountPoints += 2 -> MountInfo(1) twomanhoverbuggy.TrunkSize = InventoryTile.Tile1511 twomanhoverbuggy.TrunkOffset = 30 twomanhoverbuggy.TrunkLocation = Vector3(-3.39f, 0f, 0f) @@ -5891,21 +5892,22 @@ object GlobalDefinitions { mediumtransport.Repairable = true mediumtransport.RepairIfDestroyed = false mediumtransport.MaxShields = 500 - mediumtransport.Seats += 0 -> new SeatDefinition() - mediumtransport.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + mediumtransport.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } mediumtransport.Seats += 1 -> new SeatDefinition() - mediumtransport.Seats(1).ControlledWeapon = 5 mediumtransport.Seats += 2 -> new SeatDefinition() - mediumtransport.Seats(2).ControlledWeapon = 6 - mediumtransport.Seats += 3 -> new SeatDefinition() - mediumtransport.Seats += 4 -> new SeatDefinition() - mediumtransport.Weapons += 5 -> mediumtransport_weapon_systemA - mediumtransport.Weapons += 6 -> mediumtransport_weapon_systemB - mediumtransport.MountPoints += 1 -> 0 - mediumtransport.MountPoints += 2 -> 1 - mediumtransport.MountPoints += 3 -> 2 - mediumtransport.MountPoints += 4 -> 3 - mediumtransport.MountPoints += 5 -> 4 + mediumtransport.Seats += 3 -> new SeatDefinition() + mediumtransport.Seats += 4 -> new SeatDefinition() + mediumtransport.controlledWeapons += 1 -> 5 + mediumtransport.controlledWeapons += 2 -> 6 + mediumtransport.Weapons += 5 -> mediumtransport_weapon_systemA + mediumtransport.Weapons += 6 -> mediumtransport_weapon_systemB + mediumtransport.MountPoints += 1 -> MountInfo(0) + mediumtransport.MountPoints += 2 -> MountInfo(1) + mediumtransport.MountPoints += 3 -> MountInfo(2) + mediumtransport.MountPoints += 4 -> MountInfo(3) + mediumtransport.MountPoints += 5 -> MountInfo(4) mediumtransport.TrunkSize = InventoryTile.Tile1515 mediumtransport.TrunkOffset = 30 mediumtransport.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -5933,25 +5935,26 @@ object GlobalDefinitions { battlewagon.Repairable = true battlewagon.RepairIfDestroyed = false battlewagon.MaxShields = 500 - battlewagon.Seats += 0 -> new SeatDefinition() - battlewagon.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + battlewagon.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } battlewagon.Seats += 1 -> new SeatDefinition() - battlewagon.Seats(1).ControlledWeapon = 5 battlewagon.Seats += 2 -> new SeatDefinition() - battlewagon.Seats(2).ControlledWeapon = 6 battlewagon.Seats += 3 -> new SeatDefinition() - battlewagon.Seats(3).ControlledWeapon = 7 battlewagon.Seats += 4 -> new SeatDefinition() - battlewagon.Seats(4).ControlledWeapon = 8 + battlewagon.controlledWeapons += 1 -> 5 + battlewagon.controlledWeapons += 2 -> 6 + battlewagon.controlledWeapons += 3 -> 7 + battlewagon.controlledWeapons += 4 -> 8 battlewagon.Weapons += 5 -> battlewagon_weapon_systema battlewagon.Weapons += 6 -> battlewagon_weapon_systemb battlewagon.Weapons += 7 -> battlewagon_weapon_systemc battlewagon.Weapons += 8 -> battlewagon_weapon_systemd - battlewagon.MountPoints += 1 -> 0 - battlewagon.MountPoints += 2 -> 1 - battlewagon.MountPoints += 3 -> 2 - battlewagon.MountPoints += 4 -> 3 - battlewagon.MountPoints += 5 -> 4 + battlewagon.MountPoints += 1 -> MountInfo(0) + battlewagon.MountPoints += 2 -> MountInfo(1) + battlewagon.MountPoints += 3 -> MountInfo(2) + battlewagon.MountPoints += 4 -> MountInfo(3) + battlewagon.MountPoints += 5 -> MountInfo(4) battlewagon.TrunkSize = InventoryTile.Tile1515 battlewagon.TrunkOffset = 30 battlewagon.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -5978,21 +5981,22 @@ object GlobalDefinitions { thunderer.Repairable = true thunderer.RepairIfDestroyed = false thunderer.MaxShields = 500 - thunderer.Seats += 0 -> new SeatDefinition() - thunderer.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + thunderer.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } thunderer.Seats += 1 -> new SeatDefinition() - thunderer.Seats(1).ControlledWeapon = 5 thunderer.Seats += 2 -> new SeatDefinition() - thunderer.Seats(2).ControlledWeapon = 6 - thunderer.Seats += 3 -> new SeatDefinition() - thunderer.Seats += 4 -> new SeatDefinition() - thunderer.Weapons += 5 -> thunderer_weapon_systema - thunderer.Weapons += 6 -> thunderer_weapon_systemb - thunderer.MountPoints += 1 -> 0 - thunderer.MountPoints += 2 -> 1 - thunderer.MountPoints += 3 -> 2 - thunderer.MountPoints += 4 -> 3 - thunderer.MountPoints += 5 -> 4 + thunderer.Seats += 3 -> new SeatDefinition() + thunderer.Seats += 4 -> new SeatDefinition() + thunderer.Weapons += 5 -> thunderer_weapon_systema + thunderer.Weapons += 6 -> thunderer_weapon_systemb + thunderer.controlledWeapons += 1 -> 5 + thunderer.controlledWeapons += 2 -> 6 + thunderer.MountPoints += 1 -> MountInfo(0) + thunderer.MountPoints += 2 -> MountInfo(1) + thunderer.MountPoints += 3 -> MountInfo(2) + thunderer.MountPoints += 4 -> MountInfo(3) + thunderer.MountPoints += 5 -> MountInfo(4) thunderer.TrunkSize = InventoryTile.Tile1515 thunderer.TrunkOffset = 30 thunderer.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -6020,21 +6024,22 @@ object GlobalDefinitions { aurora.Repairable = true aurora.RepairIfDestroyed = false aurora.MaxShields = 500 - aurora.Seats += 0 -> new SeatDefinition() - aurora.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + aurora.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } aurora.Seats += 1 -> new SeatDefinition() - aurora.Seats(1).ControlledWeapon = 5 aurora.Seats += 2 -> new SeatDefinition() - aurora.Seats(2).ControlledWeapon = 6 - aurora.Seats += 3 -> new SeatDefinition() - aurora.Seats += 4 -> new SeatDefinition() - aurora.Weapons += 5 -> aurora_weapon_systema - aurora.Weapons += 6 -> aurora_weapon_systemb - aurora.MountPoints += 1 -> 0 - aurora.MountPoints += 2 -> 1 - aurora.MountPoints += 3 -> 2 - aurora.MountPoints += 4 -> 3 - aurora.MountPoints += 5 -> 4 + aurora.Seats += 3 -> new SeatDefinition() + aurora.Seats += 4 -> new SeatDefinition() + aurora.controlledWeapons += 1 -> 5 + aurora.controlledWeapons += 2 -> 6 + aurora.Weapons += 5 -> aurora_weapon_systema + aurora.Weapons += 6 -> aurora_weapon_systemb + aurora.MountPoints += 1 -> MountInfo(0) + aurora.MountPoints += 2 -> MountInfo(1) + aurora.MountPoints += 3 -> MountInfo(2) + aurora.MountPoints += 4 -> MountInfo(3) + aurora.MountPoints += 5 -> MountInfo(4) aurora.TrunkSize = InventoryTile.Tile1515 aurora.TrunkOffset = 30 aurora.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -6062,43 +6067,41 @@ object GlobalDefinitions { apc_tr.Repairable = true apc_tr.RepairIfDestroyed = false apc_tr.MaxShields = 1200 - apc_tr.Seats += 0 -> new SeatDefinition() - apc_tr.Seats += 1 -> new SeatDefinition() - apc_tr.Seats(1).ControlledWeapon = 11 - apc_tr.Seats += 2 -> new SeatDefinition() - apc_tr.Seats(2).ControlledWeapon = 12 - apc_tr.Seats += 3 -> new SeatDefinition() - apc_tr.Seats += 4 -> new SeatDefinition() - apc_tr.Seats += 5 -> new SeatDefinition() - apc_tr.Seats(5).ControlledWeapon = 15 - apc_tr.Seats += 6 -> new SeatDefinition() - apc_tr.Seats(6).ControlledWeapon = 16 - apc_tr.Seats += 7 -> new SeatDefinition() - apc_tr.Seats(7).ControlledWeapon = 13 - apc_tr.Seats += 8 -> new SeatDefinition() - apc_tr.Seats(8).ControlledWeapon = 14 - apc_tr.Seats += 9 -> new SeatDefinition() - apc_tr.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_tr.Seats += 10 -> new SeatDefinition() - apc_tr.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_tr.Weapons += 11 -> apc_weapon_systemc_tr - apc_tr.Weapons += 12 -> apc_weapon_systemb - apc_tr.Weapons += 13 -> apc_weapon_systema - apc_tr.Weapons += 14 -> apc_weapon_systemd_tr - apc_tr.Weapons += 15 -> apc_ballgun_r - apc_tr.Weapons += 16 -> apc_ballgun_l - apc_tr.MountPoints += 1 -> 0 - apc_tr.MountPoints += 2 -> 0 - apc_tr.MountPoints += 3 -> 1 - apc_tr.MountPoints += 4 -> 2 - apc_tr.MountPoints += 5 -> 3 - apc_tr.MountPoints += 6 -> 4 - apc_tr.MountPoints += 7 -> 5 - apc_tr.MountPoints += 8 -> 6 - apc_tr.MountPoints += 9 -> 7 - apc_tr.MountPoints += 10 -> 8 - apc_tr.MountPoints += 11 -> 9 - apc_tr.MountPoints += 12 -> 10 + apc_tr.Seats += 0 -> new SeatDefinition() + apc_tr.Seats += 1 -> new SeatDefinition() + apc_tr.Seats += 2 -> new SeatDefinition() + apc_tr.Seats += 3 -> new SeatDefinition() + apc_tr.Seats += 4 -> new SeatDefinition() + apc_tr.Seats += 5 -> new SeatDefinition() + apc_tr.Seats += 6 -> new SeatDefinition() + apc_tr.Seats += 7 -> new SeatDefinition() + apc_tr.Seats += 8 -> new SeatDefinition() + apc_tr.Seats += 9 -> maxOnlySeat + apc_tr.Seats += 10 -> maxOnlySeat + apc_tr.controlledWeapons += 1 -> 11 + apc_tr.controlledWeapons += 2 -> 12 + apc_tr.controlledWeapons += 5 -> 15 + apc_tr.controlledWeapons += 6 -> 16 + apc_tr.controlledWeapons += 7 -> 13 + apc_tr.controlledWeapons += 8 -> 14 + apc_tr.Weapons += 11 -> apc_weapon_systemc_tr + apc_tr.Weapons += 12 -> apc_weapon_systemb + apc_tr.Weapons += 13 -> apc_weapon_systema + apc_tr.Weapons += 14 -> apc_weapon_systemd_tr + apc_tr.Weapons += 15 -> apc_ballgun_r + apc_tr.Weapons += 16 -> apc_ballgun_l + apc_tr.MountPoints += 1 -> MountInfo(0) + apc_tr.MountPoints += 2 -> MountInfo(0) + apc_tr.MountPoints += 3 -> MountInfo(1) + apc_tr.MountPoints += 4 -> MountInfo(2) + apc_tr.MountPoints += 5 -> MountInfo(3) + apc_tr.MountPoints += 6 -> MountInfo(4) + apc_tr.MountPoints += 7 -> MountInfo(5) + apc_tr.MountPoints += 8 -> MountInfo(6) + apc_tr.MountPoints += 9 -> MountInfo(7) + apc_tr.MountPoints += 10 -> MountInfo(8) + apc_tr.MountPoints += 11 -> MountInfo(9) + apc_tr.MountPoints += 12 -> MountInfo(10) apc_tr.TrunkSize = InventoryTile.Tile2016 apc_tr.TrunkOffset = 30 apc_tr.TrunkLocation = Vector3(-5.82f, 0f, 0f) @@ -6126,43 +6129,41 @@ object GlobalDefinitions { apc_nc.Repairable = true apc_nc.RepairIfDestroyed = false apc_nc.MaxShields = 1200 - apc_nc.Seats += 0 -> new SeatDefinition() - apc_nc.Seats += 1 -> new SeatDefinition() - apc_nc.Seats(1).ControlledWeapon = 11 - apc_nc.Seats += 2 -> new SeatDefinition() - apc_nc.Seats(2).ControlledWeapon = 12 - apc_nc.Seats += 3 -> new SeatDefinition() - apc_nc.Seats += 4 -> new SeatDefinition() - apc_nc.Seats += 5 -> new SeatDefinition() - apc_nc.Seats(5).ControlledWeapon = 15 - apc_nc.Seats += 6 -> new SeatDefinition() - apc_nc.Seats(6).ControlledWeapon = 16 - apc_nc.Seats += 7 -> new SeatDefinition() - apc_nc.Seats(7).ControlledWeapon = 13 + apc_nc.Seats += 0 -> new SeatDefinition() + apc_nc.Seats += 1 -> new SeatDefinition() + apc_nc.Seats += 2 -> new SeatDefinition() + apc_nc.Seats += 3 -> new SeatDefinition() + apc_nc.Seats += 4 -> new SeatDefinition() + apc_nc.Seats += 5 -> new SeatDefinition() + apc_nc.Seats += 6 -> new SeatDefinition() + apc_nc.Seats += 7 -> new SeatDefinition() apc_nc.Seats += 8 -> new SeatDefinition() - apc_nc.Seats(8).ControlledWeapon = 14 - apc_nc.Seats += 9 -> new SeatDefinition() - apc_nc.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_nc.Seats += 10 -> new SeatDefinition() - apc_nc.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_nc.Weapons += 11 -> apc_weapon_systemc_nc - apc_nc.Weapons += 12 -> apc_weapon_systemb - apc_nc.Weapons += 13 -> apc_weapon_systema - apc_nc.Weapons += 14 -> apc_weapon_systemd_nc - apc_nc.Weapons += 15 -> apc_ballgun_r - apc_nc.Weapons += 16 -> apc_ballgun_l - apc_nc.MountPoints += 1 -> 0 - apc_nc.MountPoints += 2 -> 0 - apc_nc.MountPoints += 3 -> 1 - apc_nc.MountPoints += 4 -> 2 - apc_nc.MountPoints += 5 -> 3 - apc_nc.MountPoints += 6 -> 4 - apc_nc.MountPoints += 7 -> 5 - apc_nc.MountPoints += 8 -> 6 - apc_nc.MountPoints += 9 -> 7 - apc_nc.MountPoints += 10 -> 8 - apc_nc.MountPoints += 11 -> 9 - apc_nc.MountPoints += 12 -> 10 + apc_nc.Seats += 9 -> maxOnlySeat + apc_nc.Seats += 10 -> maxOnlySeat + apc_nc.controlledWeapons += 1 -> 11 + apc_nc.controlledWeapons += 2 -> 12 + apc_nc.controlledWeapons += 5 -> 15 + apc_nc.controlledWeapons += 6 -> 16 + apc_nc.controlledWeapons += 7 -> 13 + apc_nc.controlledWeapons += 8 -> 14 + apc_nc.Weapons += 11 -> apc_weapon_systemc_nc + apc_nc.Weapons += 12 -> apc_weapon_systemb + apc_nc.Weapons += 13 -> apc_weapon_systema + apc_nc.Weapons += 14 -> apc_weapon_systemd_nc + apc_nc.Weapons += 15 -> apc_ballgun_r + apc_nc.Weapons += 16 -> apc_ballgun_l + apc_nc.MountPoints += 1 -> MountInfo(0) + apc_nc.MountPoints += 2 -> MountInfo(0) + apc_nc.MountPoints += 3 -> MountInfo(1) + apc_nc.MountPoints += 4 -> MountInfo(2) + apc_nc.MountPoints += 5 -> MountInfo(3) + apc_nc.MountPoints += 6 -> MountInfo(4) + apc_nc.MountPoints += 7 -> MountInfo(5) + apc_nc.MountPoints += 8 -> MountInfo(6) + apc_nc.MountPoints += 9 -> MountInfo(7) + apc_nc.MountPoints += 10 -> MountInfo(8) + apc_nc.MountPoints += 11 -> MountInfo(9) + apc_nc.MountPoints += 12 -> MountInfo(10) apc_nc.TrunkSize = InventoryTile.Tile2016 apc_nc.TrunkOffset = 30 apc_nc.TrunkLocation = Vector3(-5.82f, 0f, 0f) @@ -6190,43 +6191,41 @@ object GlobalDefinitions { apc_vs.Repairable = true apc_vs.RepairIfDestroyed = false apc_vs.MaxShields = 1200 - apc_vs.Seats += 0 -> new SeatDefinition() - apc_vs.Seats += 1 -> new SeatDefinition() - apc_vs.Seats(1).ControlledWeapon = 11 - apc_vs.Seats += 2 -> new SeatDefinition() - apc_vs.Seats(2).ControlledWeapon = 12 - apc_vs.Seats += 3 -> new SeatDefinition() - apc_vs.Seats += 4 -> new SeatDefinition() - apc_vs.Seats += 5 -> new SeatDefinition() - apc_vs.Seats(5).ControlledWeapon = 15 - apc_vs.Seats += 6 -> new SeatDefinition() - apc_vs.Seats(6).ControlledWeapon = 16 - apc_vs.Seats += 7 -> new SeatDefinition() - apc_vs.Seats(7).ControlledWeapon = 13 - apc_vs.Seats += 8 -> new SeatDefinition() - apc_vs.Seats(8).ControlledWeapon = 14 - apc_vs.Seats += 9 -> new SeatDefinition() - apc_vs.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_vs.Seats += 10 -> new SeatDefinition() - apc_vs.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_vs.Weapons += 11 -> apc_weapon_systemc_vs - apc_vs.Weapons += 12 -> apc_weapon_systemb - apc_vs.Weapons += 13 -> apc_weapon_systema - apc_vs.Weapons += 14 -> apc_weapon_systemd_vs - apc_vs.Weapons += 15 -> apc_ballgun_r - apc_vs.Weapons += 16 -> apc_ballgun_l - apc_vs.MountPoints += 1 -> 0 - apc_vs.MountPoints += 2 -> 0 - apc_vs.MountPoints += 3 -> 1 - apc_vs.MountPoints += 4 -> 2 - apc_vs.MountPoints += 5 -> 3 - apc_vs.MountPoints += 6 -> 4 - apc_vs.MountPoints += 7 -> 5 - apc_vs.MountPoints += 8 -> 6 - apc_vs.MountPoints += 9 -> 7 - apc_vs.MountPoints += 10 -> 8 - apc_vs.MountPoints += 11 -> 9 - apc_vs.MountPoints += 12 -> 10 + apc_vs.Seats += 0 -> new SeatDefinition() + apc_vs.Seats += 1 -> new SeatDefinition() + apc_vs.Seats += 2 -> new SeatDefinition() + apc_vs.Seats += 3 -> new SeatDefinition() + apc_vs.Seats += 4 -> new SeatDefinition() + apc_vs.Seats += 5 -> new SeatDefinition() + apc_vs.Seats += 6 -> new SeatDefinition() + apc_vs.Seats += 7 -> new SeatDefinition() + apc_vs.Seats += 8 -> new SeatDefinition() + apc_vs.Seats += 9 -> maxOnlySeat + apc_vs.Seats += 10 -> maxOnlySeat + apc_vs.controlledWeapons += 1 -> 11 + apc_vs.controlledWeapons += 2 -> 12 + apc_vs.controlledWeapons += 5 -> 15 + apc_vs.controlledWeapons += 6 -> 16 + apc_vs.controlledWeapons += 7 -> 13 + apc_vs.controlledWeapons += 8 -> 14 + apc_vs.Weapons += 11 -> apc_weapon_systemc_vs + apc_vs.Weapons += 12 -> apc_weapon_systemb + apc_vs.Weapons += 13 -> apc_weapon_systema + apc_vs.Weapons += 14 -> apc_weapon_systemd_vs + apc_vs.Weapons += 15 -> apc_ballgun_r + apc_vs.Weapons += 16 -> apc_ballgun_l + apc_vs.MountPoints += 1 -> MountInfo(0) + apc_vs.MountPoints += 2 -> MountInfo(0) + apc_vs.MountPoints += 3 -> MountInfo(1) + apc_vs.MountPoints += 4 -> MountInfo(2) + apc_vs.MountPoints += 5 -> MountInfo(3) + apc_vs.MountPoints += 6 -> MountInfo(4) + apc_vs.MountPoints += 7 -> MountInfo(5) + apc_vs.MountPoints += 8 -> MountInfo(6) + apc_vs.MountPoints += 9 -> MountInfo(7) + apc_vs.MountPoints += 10 -> MountInfo(8) + apc_vs.MountPoints += 11 -> MountInfo(9) + apc_vs.MountPoints += 12 -> MountInfo(10) apc_vs.TrunkSize = InventoryTile.Tile2016 apc_vs.TrunkOffset = 30 apc_vs.TrunkLocation = Vector3(-5.82f, 0f, 0f) @@ -6254,12 +6253,13 @@ object GlobalDefinitions { lightning.Repairable = true lightning.RepairIfDestroyed = false lightning.MaxShields = 400 - lightning.Seats += 0 -> new SeatDefinition() - lightning.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - lightning.Seats(0).ControlledWeapon = 1 - lightning.Weapons += 1 -> lightning_weapon_system - lightning.MountPoints += 1 -> 0 - lightning.MountPoints += 2 -> 0 + lightning.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } + lightning.controlledWeapons += 0 -> 1 + lightning.Weapons += 1 -> lightning_weapon_system + lightning.MountPoints += 1 -> MountInfo(0) + lightning.MountPoints += 2 -> MountInfo(0) lightning.TrunkSize = InventoryTile.Tile1511 lightning.TrunkOffset = 30 lightning.TrunkLocation = Vector3(-3f, 0f, 0f) @@ -6287,17 +6287,18 @@ object GlobalDefinitions { prowler.Repairable = true prowler.RepairIfDestroyed = false prowler.MaxShields = 960 - prowler.Seats += 0 -> new SeatDefinition() - prowler.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + prowler.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } prowler.Seats += 1 -> new SeatDefinition() - prowler.Seats(1).ControlledWeapon = 3 prowler.Seats += 2 -> new SeatDefinition() - prowler.Seats(2).ControlledWeapon = 4 + prowler.controlledWeapons += 1 -> 3 + prowler.controlledWeapons += 2 -> 4 prowler.Weapons += 3 -> prowler_weapon_systemA prowler.Weapons += 4 -> prowler_weapon_systemB - prowler.MountPoints += 1 -> 0 - prowler.MountPoints += 2 -> 1 - prowler.MountPoints += 3 -> 2 + prowler.MountPoints += 1 -> MountInfo(0) + prowler.MountPoints += 2 -> MountInfo(1) + prowler.MountPoints += 3 -> MountInfo(2) prowler.TrunkSize = InventoryTile.Tile1511 prowler.TrunkOffset = 30 prowler.TrunkLocation = Vector3(-4.71f, 0f, 0f) @@ -6325,13 +6326,14 @@ object GlobalDefinitions { vanguard.Repairable = true vanguard.RepairIfDestroyed = false vanguard.MaxShields = 1080 - vanguard.Seats += 0 -> new SeatDefinition() - vanguard.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + vanguard.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } vanguard.Seats += 1 -> new SeatDefinition() - vanguard.Seats(1).ControlledWeapon = 2 - vanguard.Weapons += 2 -> vanguard_weapon_system - vanguard.MountPoints += 1 -> 0 - vanguard.MountPoints += 2 -> 1 + vanguard.controlledWeapons += 1 -> 2 + vanguard.Weapons += 2 -> vanguard_weapon_system + vanguard.MountPoints += 1 -> MountInfo(0) + vanguard.MountPoints += 2 -> MountInfo(1) vanguard.TrunkSize = InventoryTile.Tile1511 vanguard.TrunkOffset = 30 vanguard.TrunkLocation = Vector3(-4.84f, 0f, 0f) @@ -6359,15 +6361,16 @@ object GlobalDefinitions { magrider.Repairable = true magrider.RepairIfDestroyed = false magrider.MaxShields = 840 - magrider.Seats += 0 -> new SeatDefinition() - magrider.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - magrider.Seats(0).ControlledWeapon = 2 + magrider.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } magrider.Seats += 1 -> new SeatDefinition() - magrider.Seats(1).ControlledWeapon = 3 - magrider.Weapons += 2 -> particle_beam_magrider - magrider.Weapons += 3 -> heavy_rail_beam_magrider - magrider.MountPoints += 1 -> 0 - magrider.MountPoints += 2 -> 1 + magrider.controlledWeapons += 0 -> 2 + magrider.controlledWeapons += 1 -> 3 + magrider.Weapons += 2 -> particle_beam_magrider + magrider.Weapons += 3 -> heavy_rail_beam_magrider + magrider.MountPoints += 1 -> MountInfo(0) + magrider.MountPoints += 2 -> MountInfo(1) magrider.TrunkSize = InventoryTile.Tile1511 magrider.TrunkOffset = 30 magrider.TrunkLocation = Vector3(5.06f, 0f, 0f) @@ -6396,10 +6399,11 @@ object GlobalDefinitions { ant.Repairable = true ant.RepairIfDestroyed = false ant.MaxShields = 400 - ant.Seats += 0 -> new SeatDefinition() - ant.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - ant.MountPoints += 1 -> 0 - ant.MountPoints += 2 -> 0 + ant.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } + ant.MountPoints += 1 -> MountInfo(0) + ant.MountPoints += 2 -> MountInfo(0) ant.Deployment = true ant.DeployTime = 1500 ant.UndeployTime = 1500 @@ -6429,10 +6433,11 @@ object GlobalDefinitions { ams.Repairable = true ams.RepairIfDestroyed = false ams.MaxShields = 1000 // Temporary - original value is 600 + 1 - ams.Seats += 0 -> new SeatDefinition() - ams.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - ams.MountPoints += 1 -> 0 - ams.MountPoints += 2 -> 0 + ams.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } + ams.MountPoints += 1 -> MountInfo(0) + ams.MountPoints += 2 -> MountInfo(0) ams.Utilities += 1 -> UtilityType.matrix_terminalc ams.Utilities += 2 -> UtilityType.ams_respawn_tube ams.Utilities += 3 -> UtilityType.order_terminala @@ -6468,7 +6473,7 @@ object GlobalDefinitions { router.RepairIfDestroyed = false router.MaxShields = 800 router.Seats += 0 -> new SeatDefinition() - router.MountPoints += 1 -> 0 + router.MountPoints += 1 -> MountInfo(0) router.Utilities += 1 -> UtilityType.teleportpad_terminal router.Utilities += 2 -> UtilityType.internal_router_telepad_deployable router.TrunkSize = InventoryTile.Tile1511 @@ -6504,10 +6509,10 @@ object GlobalDefinitions { switchblade.RepairIfDestroyed = false switchblade.MaxShields = 350 switchblade.Seats += 0 -> new SeatDefinition() - switchblade.Seats(0).ControlledWeapon = 1 + switchblade.controlledWeapons += 0 -> 1 switchblade.Weapons += 1 -> scythe - switchblade.MountPoints += 1 -> 0 - switchblade.MountPoints += 2 -> 0 + switchblade.MountPoints += 1 -> MountInfo(0) + switchblade.MountPoints += 2 -> MountInfo(0) switchblade.TrunkSize = InventoryTile.Tile1511 switchblade.TrunkOffset = 30 switchblade.TrunkLocation = Vector3(-2.5f, 0f, 0f) @@ -6541,9 +6546,9 @@ object GlobalDefinitions { flail.RepairIfDestroyed = false flail.MaxShields = 480 flail.Seats += 0 -> new SeatDefinition() - flail.Seats(0).ControlledWeapon = 1 - flail.Weapons += 1 -> flail_weapon - flail.MountPoints += 1 -> 0 + flail.controlledWeapons += 0 -> 1 + flail.Weapons += 1 -> flail_weapon + flail.MountPoints += 1 -> MountInfo(0) flail.TrunkSize = InventoryTile.Tile1511 flail.TrunkOffset = 30 flail.TrunkLocation = Vector3(-3.75f, 0f, 0f) @@ -6576,12 +6581,11 @@ object GlobalDefinitions { mosquito.RepairIfDestroyed = false mosquito.MaxShields = 133 mosquito.CanFly = true - mosquito.Seats += 0 -> new SeatDefinition() - mosquito.Seats(0).Bailable = true - mosquito.Seats(0).ControlledWeapon = 1 + mosquito.Seats += 0 -> bailableSeat + mosquito.controlledWeapons += 0 -> 1 mosquito.Weapons += 1 -> rotarychaingun_mosquito - mosquito.MountPoints += 1 -> 0 - mosquito.MountPoints += 2 -> 0 + mosquito.MountPoints += 1 -> MountInfo(0) + mosquito.MountPoints += 2 -> MountInfo(0) mosquito.TrunkSize = InventoryTile.Tile1111 mosquito.TrunkOffset = 30 mosquito.TrunkLocation = Vector3(-4.6f, 0f, 0f) @@ -6610,12 +6614,11 @@ object GlobalDefinitions { lightgunship.RepairIfDestroyed = false lightgunship.MaxShields = 200 lightgunship.CanFly = true - lightgunship.Seats += 0 -> new SeatDefinition() - lightgunship.Seats(0).Bailable = true - lightgunship.Seats(0).ControlledWeapon = 1 + lightgunship.Seats += 0 -> bailableSeat + lightgunship.controlledWeapons += 0 -> 1 lightgunship.Weapons += 1 -> lightgunship_weapon_system - lightgunship.MountPoints += 1 -> 0 - lightgunship.MountPoints += 2 -> 0 + lightgunship.MountPoints += 1 -> MountInfo(0) + lightgunship.MountPoints += 2 -> MountInfo(0) lightgunship.TrunkSize = InventoryTile.Tile1511 lightgunship.TrunkOffset = 30 lightgunship.TrunkLocation = Vector3(-5.61f, 0f, 0f) @@ -6645,12 +6648,11 @@ object GlobalDefinitions { wasp.RepairIfDestroyed = false wasp.MaxShields = 103 wasp.CanFly = true - wasp.Seats += 0 -> new SeatDefinition() - wasp.Seats(0).Bailable = true - wasp.Seats(0).ControlledWeapon = 1 + wasp.Seats += 0 -> bailableSeat + wasp.controlledWeapons += 0 -> 1 wasp.Weapons += 1 -> wasp_weapon_system - wasp.MountPoints += 1 -> 0 - wasp.MountPoints += 2 -> 0 + wasp.MountPoints += 1 -> MountInfo(0) + wasp.MountPoints += 2 -> MountInfo(0) wasp.TrunkSize = InventoryTile.Tile1111 wasp.TrunkOffset = 30 wasp.TrunkLocation = Vector3(-4.6f, 0f, 0f) @@ -6680,20 +6682,18 @@ object GlobalDefinitions { liberator.MaxShields = 500 liberator.CanFly = true liberator.Seats += 0 -> new SeatDefinition() - liberator.Seats(0).ControlledWeapon = 3 - liberator.Seats += 1 -> new SeatDefinition() - liberator.Seats(1).ControlledWeapon = 4 - liberator.Seats(1).Bailable = true - liberator.Seats += 2 -> new SeatDefinition() - liberator.Seats(2).ControlledWeapon = 5 - liberator.Seats(2).Bailable = true + liberator.Seats += 1 -> bailableSeat + liberator.Seats += 2 -> bailableSeat + liberator.controlledWeapons += 0 -> 3 + liberator.controlledWeapons += 1 -> 4 + liberator.controlledWeapons += 2 -> 5 liberator.Weapons += 3 -> liberator_weapon_system liberator.Weapons += 4 -> liberator_bomb_bay liberator.Weapons += 5 -> liberator_25mm_cannon - liberator.MountPoints += 1 -> 0 - liberator.MountPoints += 2 -> 1 - liberator.MountPoints += 3 -> 1 - liberator.MountPoints += 4 -> 2 + liberator.MountPoints += 1 -> MountInfo(0) + liberator.MountPoints += 2 -> MountInfo(1) + liberator.MountPoints += 3 -> MountInfo(1) + liberator.MountPoints += 4 -> MountInfo(2) liberator.TrunkSize = InventoryTile.Tile1515 liberator.TrunkOffset = 30 liberator.TrunkLocation = Vector3(-0.76f, -1.88f, 0f) @@ -6724,20 +6724,18 @@ object GlobalDefinitions { vulture.MaxShields = 500 vulture.CanFly = true vulture.Seats += 0 -> new SeatDefinition() - vulture.Seats(0).ControlledWeapon = 3 - vulture.Seats += 1 -> new SeatDefinition() - vulture.Seats(1).ControlledWeapon = 4 - vulture.Seats(1).Bailable = true - vulture.Seats += 2 -> new SeatDefinition() - vulture.Seats(2).ControlledWeapon = 5 - vulture.Seats(2).Bailable = true + vulture.Seats += 1 -> bailableSeat + vulture.Seats += 2 -> bailableSeat + vulture.controlledWeapons += 0 -> 3 + vulture.controlledWeapons += 1 -> 4 + vulture.controlledWeapons += 2 -> 5 vulture.Weapons += 3 -> vulture_nose_weapon_system vulture.Weapons += 4 -> vulture_bomb_bay vulture.Weapons += 5 -> vulture_tail_cannon - vulture.MountPoints += 1 -> 0 - vulture.MountPoints += 2 -> 1 - vulture.MountPoints += 3 -> 1 - vulture.MountPoints += 4 -> 2 + vulture.MountPoints += 1 -> MountInfo(0) + vulture.MountPoints += 2 -> MountInfo(1) + vulture.MountPoints += 3 -> MountInfo(1) + vulture.MountPoints += 4 -> MountInfo(2) vulture.TrunkSize = InventoryTile.Tile1611 vulture.TrunkOffset = 30 vulture.TrunkLocation = Vector3(-0.76f, -1.88f, 0f) @@ -6770,50 +6768,46 @@ object GlobalDefinitions { dropship.MaxShields = 1000 dropship.CanFly = true dropship.Seats += 0 -> new SeatDefinition() - dropship.Seats += 1 -> new SeatDefinition() - dropship.Seats(1).Bailable = true - dropship.Seats(1).ControlledWeapon = 12 - dropship.Seats += 2 -> new SeatDefinition() - dropship.Seats(2).Bailable = true - dropship.Seats(2).ControlledWeapon = 13 - dropship.Seats += 3 -> new SeatDefinition() - dropship.Seats(3).Bailable = true - dropship.Seats += 4 -> new SeatDefinition() - dropship.Seats(4).Bailable = true - dropship.Seats += 5 -> new SeatDefinition() - dropship.Seats(5).Bailable = true - dropship.Seats += 6 -> new SeatDefinition() - dropship.Seats(6).Bailable = true - dropship.Seats += 7 -> new SeatDefinition() - dropship.Seats(7).Bailable = true - dropship.Seats += 8 -> new SeatDefinition() - dropship.Seats(8).Bailable = true - dropship.Seats += 9 -> new SeatDefinition() - dropship.Seats(9).Bailable = true - dropship.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - dropship.Seats += 10 -> new SeatDefinition() - dropship.Seats(10).Bailable = true - dropship.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - dropship.Seats += 11 -> new SeatDefinition() - dropship.Seats(11).Bailable = true - dropship.Seats(11).ControlledWeapon = 14 + dropship.Seats += 1 -> new SeatDefinition() { + bailable = true + } + dropship.Seats += 2 -> bailableSeat + dropship.Seats += 3 -> bailableSeat + dropship.Seats += 4 -> bailableSeat + dropship.Seats += 5 -> bailableSeat + dropship.Seats += 6 -> bailableSeat + dropship.Seats += 7 -> bailableSeat + dropship.Seats += 9 -> new SeatDefinition() { + bailable = true + restriction = MaxOnly + } + dropship.Seats += 10 -> new SeatDefinition() { + bailable = true + restriction = MaxOnly + } + dropship.Seats += 11 -> bailableSeat + dropship.controlledWeapons += 1 -> 12 + dropship.controlledWeapons += 2 -> 13 + dropship.controlledWeapons += 11 -> 14 dropship.Weapons += 12 -> cannon_dropship_20mm dropship.Weapons += 13 -> cannon_dropship_20mm dropship.Weapons += 14 -> dropship_rear_turret - dropship.Cargo += 15 -> new CargoDefinition() - dropship.MountPoints += 1 -> 0 - dropship.MountPoints += 2 -> 11 - dropship.MountPoints += 3 -> 1 - dropship.MountPoints += 4 -> 2 - dropship.MountPoints += 5 -> 3 - dropship.MountPoints += 6 -> 4 - dropship.MountPoints += 7 -> 5 - dropship.MountPoints += 8 -> 6 - dropship.MountPoints += 9 -> 7 - dropship.MountPoints += 10 -> 8 - dropship.MountPoints += 11 -> 9 - dropship.MountPoints += 12 -> 10 - dropship.MountPoints += 13 -> 15 + dropship.Cargo += 15 -> new CargoDefinition() { + restriction = SmallCargo + } + dropship.MountPoints += 1 -> MountInfo(0) + dropship.MountPoints += 2 -> MountInfo(11) + dropship.MountPoints += 3 -> MountInfo(1) + dropship.MountPoints += 4 -> MountInfo(2) + dropship.MountPoints += 5 -> MountInfo(3) + dropship.MountPoints += 6 -> MountInfo(4) + dropship.MountPoints += 7 -> MountInfo(5) + dropship.MountPoints += 8 -> MountInfo(6) + dropship.MountPoints += 9 -> MountInfo(7) + dropship.MountPoints += 10 -> MountInfo(8) + dropship.MountPoints += 11 -> MountInfo(9) + dropship.MountPoints += 12 -> MountInfo(10) + dropship.MountPoints += 13 -> MountInfo(15) dropship.TrunkSize = InventoryTile.Tile1612 dropship.TrunkOffset = 30 dropship.TrunkLocation = Vector3(-7.39f, -4.96f, 0f) @@ -6845,32 +6839,27 @@ object GlobalDefinitions { galaxy_gunship.MaxShields = 1200 galaxy_gunship.CanFly = true galaxy_gunship.Seats += 0 -> new SeatDefinition() - galaxy_gunship.Seats += 1 -> new SeatDefinition() - galaxy_gunship.Seats(1).ControlledWeapon = 6 - galaxy_gunship.Seats(1).Bailable = true - galaxy_gunship.Seats += 2 -> new SeatDefinition() - galaxy_gunship.Seats(2).ControlledWeapon = 7 - galaxy_gunship.Seats(2).Bailable = true - galaxy_gunship.Seats += 3 -> new SeatDefinition() - galaxy_gunship.Seats(3).ControlledWeapon = 8 - galaxy_gunship.Seats(3).Bailable = true - galaxy_gunship.Seats += 4 -> new SeatDefinition() - galaxy_gunship.Seats(4).ControlledWeapon = 9 - galaxy_gunship.Seats(4).Bailable = true - galaxy_gunship.Seats += 5 -> new SeatDefinition() - galaxy_gunship.Seats(5).ControlledWeapon = 10 - galaxy_gunship.Seats(5).Bailable = true + galaxy_gunship.Seats += 1 -> bailableSeat + galaxy_gunship.Seats += 2 -> bailableSeat + galaxy_gunship.Seats += 3 -> bailableSeat + galaxy_gunship.Seats += 4 -> bailableSeat + galaxy_gunship.Seats += 5 -> bailableSeat + galaxy_gunship.controlledWeapons += 1 -> 6 + galaxy_gunship.controlledWeapons += 2 -> 7 + galaxy_gunship.controlledWeapons += 3 -> 8 + galaxy_gunship.controlledWeapons += 4 -> 9 + galaxy_gunship.controlledWeapons += 5 -> 10 galaxy_gunship.Weapons += 6 -> galaxy_gunship_cannon galaxy_gunship.Weapons += 7 -> galaxy_gunship_cannon galaxy_gunship.Weapons += 8 -> galaxy_gunship_tailgun galaxy_gunship.Weapons += 9 -> galaxy_gunship_gun galaxy_gunship.Weapons += 10 -> galaxy_gunship_gun - galaxy_gunship.MountPoints += 1 -> 0 - galaxy_gunship.MountPoints += 2 -> 3 - galaxy_gunship.MountPoints += 3 -> 1 - galaxy_gunship.MountPoints += 4 -> 2 - galaxy_gunship.MountPoints += 5 -> 4 - galaxy_gunship.MountPoints += 6 -> 5 + galaxy_gunship.MountPoints += 1 -> MountInfo(0) + galaxy_gunship.MountPoints += 2 -> MountInfo(3) + galaxy_gunship.MountPoints += 3 -> MountInfo(1) + galaxy_gunship.MountPoints += 4 -> MountInfo(2) + galaxy_gunship.MountPoints += 5 -> MountInfo(4) + galaxy_gunship.MountPoints += 6 -> MountInfo(5) galaxy_gunship.TrunkSize = InventoryTile.Tile1816 galaxy_gunship.TrunkOffset = 30 galaxy_gunship.TrunkLocation = Vector3(-9.85f, 0f, 0f) @@ -6904,8 +6893,8 @@ object GlobalDefinitions { lodestar.MaxShields = 1000 lodestar.CanFly = true lodestar.Seats += 0 -> new SeatDefinition() - lodestar.MountPoints += 1 -> 0 - lodestar.MountPoints += 2 -> 1 + lodestar.MountPoints += 1 -> MountInfo(0) + lodestar.MountPoints += 2 -> MountInfo(1) lodestar.Cargo += 1 -> new CargoDefinition() lodestar.Utilities += 2 -> UtilityType.lodestar_repair_terminal lodestar.UtilityOffset += 2 -> Vector3(0, 20, 0) @@ -6946,19 +6935,15 @@ object GlobalDefinitions { phantasm.CanCloak = true phantasm.CanFly = true phantasm.Seats += 0 -> new SeatDefinition() - phantasm.Seats += 1 -> new SeatDefinition() - phantasm.Seats(1).Bailable = true - phantasm.Seats += 2 -> new SeatDefinition() - phantasm.Seats(2).Bailable = true - phantasm.Seats += 3 -> new SeatDefinition() - phantasm.Seats(3).Bailable = true - phantasm.Seats += 4 -> new SeatDefinition() - phantasm.Seats(4).Bailable = true - phantasm.MountPoints += 1 -> 0 - phantasm.MountPoints += 2 -> 1 - phantasm.MountPoints += 3 -> 2 - phantasm.MountPoints += 4 -> 3 - phantasm.MountPoints += 5 -> 4 + phantasm.Seats += 1 -> bailableSeat + phantasm.Seats += 2 -> bailableSeat + phantasm.Seats += 3 -> bailableSeat + phantasm.Seats += 4 -> bailableSeat + phantasm.MountPoints += 1 -> MountInfo(0) + phantasm.MountPoints += 2 -> MountInfo(1) + phantasm.MountPoints += 3 -> MountInfo(2) + phantasm.MountPoints += 4 -> MountInfo(3) + phantasm.MountPoints += 5 -> MountInfo(4) phantasm.TrunkSize = InventoryTile.Tile1107 phantasm.TrunkOffset = 30 phantasm.TrunkLocation = Vector3(-6.16f, 0f, 0f) @@ -6982,17 +6967,51 @@ object GlobalDefinitions { droppod.Name = "droppod" droppod.MaxHealth = 20000 - //droppod.Damageable = false + droppod.Damageable = false + droppod.Repairable = false droppod.CanFly = true - droppod.Seats += 0 -> new SeatDefinition - droppod.MountPoints += 1 -> 0 + droppod.Seats += 0 -> new SeatDefinition { + restriction = Unrestricted + } + droppod.MountPoints += 1 -> MountInfo(0) droppod.TrunkSize = InventoryTile.None droppod.Packet = new DroppodConverter() droppod.DeconstructionTime = Some(5 seconds) droppod.DestroyedModel = None //the adb calls out a droppod; the cyclic nature of this confounds me droppod.DamageUsing = DamageCalculations.AgainstAircraft droppod.DrownAtMaxDepth = false - //TODO geometry? + + orbital_shuttle.Name = "orbital_shuttle" + orbital_shuttle.MaxHealth = 20000 + orbital_shuttle.Damageable = false + orbital_shuttle.Repairable = false + orbital_shuttle.CanFly = true + orbital_shuttle.CanBeOwned = None + orbital_shuttle.undergoesDecay = false + orbital_shuttle.Seats += 0 -> new SeatDefinition { + occupancy = 300 + restriction = Unrestricted + } + /* + these are close to the mount point offsets in the ADB; + physically, they correlate to positions in the HART building rather than with the shuttle model by itself; + set the shuttle pad based on the zonemap extraction values then position the shuttle relative to that pad; + rotation based on the shuttle should place these offsets in the HART lobby whose gantry hall corresponds to that mount index + */ + orbital_shuttle.MountPoints += 1 -> MountInfo(0, Vector3(-62, 4, -28.2f)) + orbital_shuttle.MountPoints += 2 -> MountInfo(0, Vector3(-62, 28, -28.2f)) + orbital_shuttle.MountPoints += 3 -> MountInfo(0, Vector3(-62, 4, -18.2f)) + orbital_shuttle.MountPoints += 4 -> MountInfo(0, Vector3(-62, 28, -18.2f)) + orbital_shuttle.MountPoints += 5 -> MountInfo(0, Vector3( 62, 4, -28.2f)) + orbital_shuttle.MountPoints += 6 -> MountInfo(0, Vector3( 62, 28, -28.2f)) + orbital_shuttle.MountPoints += 7 -> MountInfo(0, Vector3( 62, 4, -18.2f)) + orbital_shuttle.MountPoints += 8 -> MountInfo(0, Vector3( 62, 28, -18.2f)) + orbital_shuttle.TrunkSize = InventoryTile.None + orbital_shuttle.Packet = new OrbitalShuttleConverter + orbital_shuttle.DeconstructionTime = None + orbital_shuttle.DestroyedModel = None + orbital_shuttle.DamageUsing = DamageCalculations.AgainstNothing + orbital_shuttle.DrownAtMaxDepth = false } /** @@ -7065,8 +7084,8 @@ object GlobalDefinitions { spitfire_turret.Damageable = true spitfire_turret.Repairable = true spitfire_turret.RepairIfDestroyed = false - spitfire_turret.Weapons += 1 -> new mutable.HashMap() - spitfire_turret.Weapons(1) += TurretUpgrade.None -> spitfire_weapon + spitfire_turret.WeaponPaths += 1 -> new mutable.HashMap() + spitfire_turret.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon spitfire_turret.ReserveAmmunition = false spitfire_turret.DeployCategory = DeployableCategory.SmallTurrets spitfire_turret.DeployTime = Duration.create(5000, "ms") @@ -7089,8 +7108,8 @@ object GlobalDefinitions { spitfire_cloaked.Damageable = true spitfire_cloaked.Repairable = true spitfire_cloaked.RepairIfDestroyed = false - spitfire_cloaked.Weapons += 1 -> new mutable.HashMap() - spitfire_cloaked.Weapons(1) += TurretUpgrade.None -> spitfire_weapon + spitfire_cloaked.WeaponPaths += 1 -> new mutable.HashMap() + spitfire_cloaked.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon spitfire_cloaked.ReserveAmmunition = false spitfire_cloaked.DeployCategory = DeployableCategory.SmallTurrets spitfire_cloaked.DeployTime = Duration.create(5000, "ms") @@ -7112,8 +7131,8 @@ object GlobalDefinitions { spitfire_aa.Damageable = true spitfire_aa.Repairable = true spitfire_aa.RepairIfDestroyed = false - spitfire_aa.Weapons += 1 -> new mutable.HashMap() - spitfire_aa.Weapons(1) += TurretUpgrade.None -> spitfire_aa_weapon + spitfire_aa.WeaponPaths += 1 -> new mutable.HashMap() + spitfire_aa.WeaponPaths(1) += TurretUpgrade.None -> spitfire_aa_weapon spitfire_aa.ReserveAmmunition = false spitfire_aa.DeployCategory = DeployableCategory.SmallTurrets spitfire_aa.DeployTime = Duration.create(5000, "ms") @@ -7173,10 +7192,11 @@ object GlobalDefinitions { portable_manned_turret.Damageable = true portable_manned_turret.Repairable = true portable_manned_turret.RepairIfDestroyed = false - portable_manned_turret.MountPoints += 1 -> 0 - portable_manned_turret.MountPoints += 2 -> 0 - portable_manned_turret.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret.Weapons(1) += TurretUpgrade.None -> energy_gun + portable_manned_turret.controlledWeapons += 0 -> 1 + portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun + portable_manned_turret.MountPoints += 1 -> MountInfo(0) + portable_manned_turret.MountPoints += 2 -> MountInfo(0) portable_manned_turret.ReserveAmmunition = true portable_manned_turret.FactionLocked = true portable_manned_turret.Packet = fieldTurretConverter @@ -7200,10 +7220,11 @@ object GlobalDefinitions { portable_manned_turret_nc.Damageable = true portable_manned_turret_nc.Repairable = true portable_manned_turret_nc.RepairIfDestroyed = false - portable_manned_turret_nc.MountPoints += 1 -> 0 - portable_manned_turret_nc.MountPoints += 2 -> 0 - portable_manned_turret_nc.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret_nc.Weapons(1) += TurretUpgrade.None -> energy_gun_nc + portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc + portable_manned_turret_nc.controlledWeapons += 0 -> 1 + portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0) portable_manned_turret_nc.ReserveAmmunition = true portable_manned_turret_nc.FactionLocked = true portable_manned_turret_nc.Packet = fieldTurretConverter @@ -7227,10 +7248,11 @@ object GlobalDefinitions { portable_manned_turret_tr.Damageable = true portable_manned_turret_tr.Repairable = true portable_manned_turret_tr.RepairIfDestroyed = false - portable_manned_turret_tr.MountPoints += 1 -> 0 - portable_manned_turret_tr.MountPoints += 2 -> 0 - portable_manned_turret_tr.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret_tr.Weapons(1) += TurretUpgrade.None -> energy_gun_tr + portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr + portable_manned_turret_tr.controlledWeapons += 0 -> 1 + portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0) portable_manned_turret_tr.ReserveAmmunition = true portable_manned_turret_tr.FactionLocked = true portable_manned_turret_tr.Packet = fieldTurretConverter @@ -7254,10 +7276,11 @@ object GlobalDefinitions { portable_manned_turret_vs.Damageable = true portable_manned_turret_vs.Repairable = true portable_manned_turret_vs.RepairIfDestroyed = false - portable_manned_turret_vs.MountPoints += 1 -> 0 - portable_manned_turret_vs.MountPoints += 2 -> 0 - portable_manned_turret_vs.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret_vs.Weapons(1) += TurretUpgrade.None -> energy_gun_vs + portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs + portable_manned_turret_vs.controlledWeapons += 0 -> 1 + portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0) portable_manned_turret_vs.ReserveAmmunition = true portable_manned_turret_vs.FactionLocked = true portable_manned_turret_vs.Packet = fieldTurretConverter @@ -7689,6 +7712,10 @@ object GlobalDefinitions { door.Damageable = false door.Repairable = false + gr_door_mb_orb.Name = "gr_door_mb_orb" + gr_door_mb_orb.Damageable = false + gr_door_mb_orb.Repairable = false + resource_silo.Name = "resource_silo" resource_silo.Damageable = false resource_silo.Repairable = false @@ -7755,11 +7782,12 @@ object GlobalDefinitions { manned_turret.Repairable = true manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.5f) manned_turret.RepairIfDestroyed = true - manned_turret.Weapons += 1 -> new mutable.HashMap() - manned_turret.Weapons(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan - manned_turret.Weapons(1) += TurretUpgrade.AVCombo -> phalanx_avcombo - manned_turret.Weapons(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo - manned_turret.MountPoints += 1 -> 0 + manned_turret.WeaponPaths += 1 -> new mutable.HashMap() + manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan + manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo + manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo + manned_turret.controlledWeapons += 0 -> 1 + manned_turret.MountPoints += 1 -> MountInfo(0) manned_turret.FactionLocked = true manned_turret.ReserveAmmunition = false manned_turret.explodes = true @@ -7771,7 +7799,7 @@ object GlobalDefinitions { DamageAtEdge = 0.1f Modifiers = ExplodingRadialDegrade } - manned_turret.Geometry = GeometryForm.representByCylinder(radius = 1.2695f, height = 2.6875f) + manned_turret.Geometry = GeometryForm.representByCylinder(radius = 1.2695f, height = 4.042f) vanu_sentry_turret.Name = "vanu_sentry_turret" vanu_sentry_turret.MaxHealth = 1500 @@ -7780,10 +7808,11 @@ object GlobalDefinitions { vanu_sentry_turret.Repairable = true vanu_sentry_turret.autoRepair = AutoRepairStats(3.27272f, 10000, 1000, 0.5f) vanu_sentry_turret.RepairIfDestroyed = true - vanu_sentry_turret.Weapons += 1 -> new mutable.HashMap() - vanu_sentry_turret.Weapons(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon - vanu_sentry_turret.MountPoints += 1 -> 0 - vanu_sentry_turret.MountPoints += 2 -> 0 + vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap() + vanu_sentry_turret.WeaponPaths(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon + vanu_sentry_turret.controlledWeapons += 0 -> 1 + vanu_sentry_turret.MountPoints += 1 -> MountInfo(0) + vanu_sentry_turret.MountPoints += 2 -> MountInfo(0) vanu_sentry_turret.FactionLocked = false vanu_sentry_turret.ReserveAmmunition = false vanu_sentry_turret.Geometry = GeometryForm.representByCylinder(radius = 1.76311f, height = 3.984375f) @@ -7876,5 +7905,10 @@ object GlobalDefinitions { //damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m } generator.Geometry = GeometryForm.representByCylinder(radius = 1.2617f, height = 9.14063f) + + obbasemesh.Name = "obbasemesh" + obbasemesh.Descriptor = "orbital_shuttle_pad" + obbasemesh.Damageable = false + obbasemesh.Repairable = false } } diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 91dd38966..76f4eabb2 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -11,7 +11,7 @@ import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.DamageableWeaponTurret import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.repair.RepairableWeaponTurret import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret} import net.psforever.objects.vital.damage.DamageCalculations @@ -25,8 +25,6 @@ class TurretDeployable(tdef: TurretDeployableDefinition) with Hackable { WeaponTurret.LoadDefinition(this) - def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap - override def Definition = tdef } @@ -65,8 +63,7 @@ class TurretControl(turret: TurretDeployable) extends Actor with FactionAffinityBehavior.Check with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events - with MountableBehavior.TurretMount - with MountableBehavior.Dismount + with MountableBehavior with DamageableWeaponTurret with RepairableWeaponTurret { def MountableObject = turret @@ -91,6 +88,13 @@ class TurretControl(turret: TurretDeployable) case _ => ; } + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player): Boolean = { + (!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed + } + override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { super.DestructionAwareness(target, cause) Deployables.AnnounceDestroyDeployable(turret, None) diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index ec3a0be31..633cb5283 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -1,10 +1,10 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.{SeatDefinition, ToolDefinition, VehicleDefinition} -import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} +import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition} +import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.mount.{Seat, SeatDefinition} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.aura.AuraContainer @@ -18,7 +18,6 @@ import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} -import scala.annotation.tailrec import scala.concurrent.duration.FiniteDuration import scala.util.{Success, Try} @@ -33,7 +32,7 @@ import scala.util.{Success, Try} * The `Map` of `Utility` objects is given using the same inventory index positions. * Positive indices and zero are considered "represented" and must be assigned a globally unique identifier * and must be present in the containing vehicle's `ObjectCreateMessage` packet. - * The index is the seat position, reflecting the position in the zero-index inventory. + * The index is the mount position, reflecting the position in the zero-index inventory. * Negative indices are expected to be excluded from this conversion. * The value of the negative index does not have a specific meaning.
*
@@ -44,27 +43,27 @@ import scala.util.{Success, Try} * The driver is the only player that can access a vehicle's saved loadouts through a repair/rearm silo * and can procure equipment from the said silo. * The owner of a vehicle and the driver of a vehicle as mostly interchangeable terms for this reason - * and it can be summarized that the player who has access to the driver seat meets the qualifications for the "owner" - * so long as that player is the last person to have sat in that seat. - * All previous ownership information is replaced just as soon as someone else sits in the driver's seat. + * and it can be summarized that the player who has access to the driver mount meets the qualifications for the "owner" + * so long as that player is the last person to have sat in that mount. + * All previous ownership information is replaced just as soon as someone else sits in the driver's mount. * Ownership is also transferred as players die and respawn (from and to the same client) * and when they leave a continent without taking the vehicle they currently own with them. * (They also lose ownership when they leave the game, of course.)
*
* All seats have vehicle-level properties on top of their own internal properties. - * A seat has a glyph projected onto the ground when the vehicle is not moving - * that is used to mark where the seat can be accessed, as well as broadcasting the current access condition of the seat. + * A mount has a glyph projected onto the ground when the vehicle is not moving + * that is used to mark where the mount can be accessed, as well as broadcasting the current access condition of the mount. * As indicated previously, seats are composed into categories and the categories used to control access. - * The "driver" group has already been mentioned and is usually composed of a single seat, the "first" one. - * The driver seat is typically locked to the person who can sit in it - the owner - unless manually unlocked. - * Any seat besides the "driver" that has a weapon controlled from the seat is called a "gunner" seats. - * Any other seat besides the "driver" seat and "gunner" seats is called a "passenger" seat. + * The "driver" group has already been mentioned and is usually composed of a single mount, the "first" one. + * The driver mount is typically locked to the person who can sit in it - the owner - unless manually unlocked. + * Any mount besides the "driver" that has a weapon controlled from the mount is called a "gunner" seats. + * Any other mount besides the "driver" mount and "gunner" seats is called a "passenger" mount. * All of these seats are typically unlocked normally. - * The "trunk" also counts as an access group even though it is not directly attached to a seat and starts as "locked." + * The "trunk" also counts as an access group even though it is not directly attached to a mount and starts as "locked." * The categories all have their own glyphs, * sharing a red cross glyph as a "can not access" state, * and may also use their lack of visibility to express state. - * In terms of individual access, each seat can have its current occupant ejected, save for the driver's seat. + * In terms of individual access, each mount can have its current occupant ejected, save for the driver's mount. * @see `Vehicle.EquipmentUtilities` * @param vehicleDef the vehicle's definition entry; * stores and unloads pertinent information about the `Vehicle`'s configuration; @@ -72,11 +71,10 @@ import scala.util.{Success, Try} */ class Vehicle(private val vehicleDef: VehicleDefinition) extends AmenityOwner + with MountableWeapons with InteractsWithZoneEnvironment with Hackable with FactionAffinity - with Mountable - with MountedWeapons with Deployment with Vitality with OwnableByPlayer @@ -90,19 +88,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition) private var decal: Int = 0 private var trunkAccess: Option[PlanetSideGUID] = None private var jammered: Boolean = false + private var cloaked: Boolean = false - private var flying: Boolean = false + private var flying: Option[Int] = None private var capacitor: Int = 0 /** * Permissions control who gets to access different parts of the vehicle; - * the groups are Driver (seat), Gunner (seats), Passenger (seats), and the Trunk + * the groups are Driver (mount), Gunner (seats), Passenger (seats), and the Trunk */ private val groupPermissions: Array[VehicleLockState.Value] = Array(VehicleLockState.Locked, VehicleLockState.Empire, VehicleLockState.Empire, VehicleLockState.Locked) - private var seats: Map[Int, Seat] = Map.empty private var cargoHolds: Map[Int, Cargo] = Map.empty - private var weapons: Map[Int, EquipmentSlot] = Map.empty private var utilities: Map[Int, Utility] = Map() private val trunk: GridInventory = GridInventory() @@ -198,9 +195,13 @@ class Vehicle(private val vehicleDef: VehicleDefinition) Cloaked } - def Flying: Boolean = flying + def isFlying: Boolean = flying.nonEmpty - def Flying_=(isFlying: Boolean): Boolean = { + def Flying: Option[Int] = flying + + def Flying_=(isFlying: Int): Option[Int] = Flying_=(Some(isFlying)) + + def Flying_=(isFlying: Option[Int]): Option[Int] = { flying = isFlying Flying } @@ -226,17 +227,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition) Capacitor } - /** - * Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it. - * @param mountPoint an index representing the seat position / mounting point - * @return a seat number, or `None` - */ - def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { - Definition.MountPoints.get(mountPoint) - } - - def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap - /** * What are the access permissions for a position on this vehicle, seats or trunk? * @param group the group index @@ -291,24 +281,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition) None } - /** - * Get the seat at the index. - * The specified "seat" can only accommodate a player as opposed to weapon mounts which share the same indexing system. - * @param seatNumber an index representing the seat position / mounting point - * @return a `Seat`, or `None` - */ - def Seat(seatNumber: Int): Option[Seat] = { - if (seatNumber >= 0 && seatNumber < this.seats.size) { - this.seats.get(seatNumber) - } else { - None - } - } - - def Seats: Map[Int, Seat] = { - seats - } - def CargoHold(cargoNumber: Int): Option[Cargo] = { if (cargoNumber >= 0) { this.cargoHolds.get(cargoNumber) @@ -322,12 +294,12 @@ class Vehicle(private val vehicleDef: VehicleDefinition) } def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = { - if (seatNumber == 0) { + if (seatNumber == 0) { //valid in almost all cases Some(AccessPermissionGroup.Driver) } else { Seat(seatNumber) match { - case Some(seat) => - seat.ControlledWeapon match { + case Some(_) => + Definition.controlledWeapons.get(seatNumber) match { case Some(_) => Some(AccessPermissionGroup.Gunner) case None => @@ -336,50 +308,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition) case None => CargoHold(seatNumber) match { case Some(_) => - Some(AccessPermissionGroup.Passenger) + Some(AccessPermissionGroup.Passenger) //TODO confirm this case None => - None + if (seatNumber >= trunk.Offset && seatNumber < trunk.Offset + trunk.TotalCapacity) { + Some(AccessPermissionGroup.Trunk) + } else { + None + } } } } } - def Weapons: Map[Int, EquipmentSlot] = weapons - - /** - * Get the weapon at the index. - * @param wepNumber an index representing the seat position / mounting point - * @return a weapon, or `None` - */ - def ControlledWeapon(wepNumber: Int): Option[Equipment] = { - weapons.get(wepNumber) match { - case Some(mount) => - mount.Equipment - case None => - None - } - } - - /** - * Given a player who may be an occupant, retrieve an number of the seat where this player is sat. - * @param player the player - * @return a seat number, or `None` if the `player` is not actually seated in this vehicle - */ - def PassengerInSeat(player: Player): Option[Int] = recursivePassengerInSeat(seats.iterator, player) - - @tailrec private def recursivePassengerInSeat(iter: Iterator[(Int, Seat)], player: Player): Option[Int] = { - if (!iter.hasNext) { - None - } else { - val (seatNumber, seat) = iter.next() - if (seat.Occupant.contains(player)) { - Some(seatNumber) - } else { - recursivePassengerInSeat(iter, player) - } - } - } - def Utilities: Map[Int, Utility] = utilities /** @@ -415,7 +355,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) def Inventory: GridInventory = trunk - def VisibleSlots: Set[Int] = weapons.keySet + def VisibleSlots: Set[Int] = weapons.keys.toSet override def Slot(slotNum: Int): EquipmentSlot = { weapons @@ -535,7 +475,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) def PrepareGatingManifest(): VehicleManifest = { val manifest = VehicleManifest(this) - seats.collect { case (index: Int, seat: Seat) if index > 0 => seat.Occupant = None } + seats.collect { case (index: Int, seat: Seat) if index > 0 => seat.unmount(seat.occupant) } vehicleGatingManifest = Some(manifest) previousVehicleGatingManifest = None manifest @@ -676,12 +616,12 @@ object Vehicle { //create seats vehicle.seats = vdef.Seats.map[Int, Seat] { case (num: Int, definition: SeatDefinition) => - num -> Seat(definition) + num -> new Seat(definition) }.toMap // create cargo holds vehicle.cargoHolds = vdef.Cargo.map[Int, Cargo] { case (num, definition) => - num -> Cargo(definition) + num -> new Cargo(definition) }.toMap //create utilities vehicle.utilities = vdef.Utilities.map[Int, Utility] { diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index ed5a328d4..89fa4cee9 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -87,7 +87,7 @@ object Vehicles { /** * Disassociate a player from a vehicle that he owns. * The vehicle must exist in the game world on the specified continent. - * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. + * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver mount. * This is the player side of vehicle ownership removal. * @param player the player */ @@ -96,7 +96,7 @@ object Vehicles { /** * Disassociate a player from a vehicle that he owns. * The vehicle must exist in the game world on the specified continent. - * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. + * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver mount. * This is the player side of vehicle ownership removal. * @param player the player */ @@ -117,7 +117,7 @@ object Vehicles { /** * Disassociate a player from a vehicle that he owns without associating a different player as the owner. - * Set the vehicle's driver seat permissions and passenger and gunner seat permissions to "allow empire," + * Set the vehicle's driver mount permissions and passenger and gunner mount permissions to "allow empire," * then reload them for all clients. * This is the vehicle side of vehicle ownership removal. * @param player the player @@ -196,7 +196,7 @@ object Vehicles { val manifestPassengerResults = manifestPassengers.map { name => vzone.Players.exists(_.name.equals(name)) } manifestPassengerResults.forall(_ == true) && vehicle.CargoHolds.values - .collect { case hold if hold.isOccupied => AllGatedOccupantsInSameZone(hold.Occupant.get) } + .collect { case hold if hold.isOccupied => AllGatedOccupantsInSameZone(hold.occupant.get) } .forall(_ == true) case _ => false @@ -230,18 +230,18 @@ object Vehicles { val zone = target.Zone // Forcefully dismount any cargo target.CargoHolds.values.foreach(cargoHold => { - cargoHold.Occupant match { + cargoHold.occupant match { case Some(cargo: Vehicle) => - cargo.Seats(0).Occupant match { + cargo.Seats(0).occupant match { case Some(_: Player) => CargoBehavior.HandleVehicleCargoDismount( target.Zone, cargo.GUID, - bailed = target.Flying, + bailed = target.isFlying, requestedByPassenger = false, kicked = true ) - case None => + case _ => log.error("FinishHackingVehicle: vehicle in cargo hold missing driver") CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, target.GUID, target, bailed = false, requestedByPassenger = false, kicked = true) } @@ -250,9 +250,9 @@ object Vehicles { }) // Forcefully dismount all seated occupants from the vehicle target.Seats.values.foreach(seat => { - seat.Occupant match { - case Some(tplayer) => - seat.Occupant = None + seat.occupant match { + case Some(tplayer: Player) => + seat.unmount(tplayer) tplayer.VehicleSeated = None if (tplayer.HasGUID) { zone.VehicleEvents ! VehicleServiceMessage( @@ -260,11 +260,11 @@ object Vehicles { VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID) ) } - case None => ; + case _ => ; } }) // If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed. - if (target.Definition.CanFly && target.Flying) { + if (target.Definition.CanFly && target.isFlying) { // todo: Should this force the vehicle to land in the same way as when a pilot bails with passengers on board? target.Actor ! Vehicle.Deconstruct() } else { // Otherwise handle ownership transfer as normal @@ -407,4 +407,21 @@ object Vehicles { case _ => ; } } + + /** + * Find the position and angle at which an ejected player will be placed once outside of the shuttle. + * Mainly for use with the proper high altitude rapid transport (HART) shuttle and it's corresponding HART building. + * @param obj the (shuttle) vehicle + * @param mountPoint the mount point that indicates a seat + * @return the position and angle + */ + def dismountShuttle(obj: Vehicle, mountPoint: Int): (Vector3, Float) = { + val shuttleAngle = obj.Orientation.z + val offset = { + val baseOffset = obj.MountPoints(mountPoint).positionOffset + Vector3.Rz(baseOffset.xy, shuttleAngle) + Vector3.z(baseOffset.z) + } + val turnAway = if (offset.x >= 0) -90f else 90f + (obj.Position + offset, (shuttleAngle + turnAway) % 360f) + } } diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index a96b8924e..e50a376b4 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -27,9 +27,11 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.objects.locker.LockerContainerControl import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} +import net.psforever.services.hart.ShuttleState import scala.concurrent.duration._ @@ -60,6 +62,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) + SetInteraction(EnvironmentAttribute.GantryDenialField, doInteractingWithGantryField) SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) private[this] val log = org.log4s.getLogger(player.Name) @@ -713,13 +716,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm ) //align client interface fields with state zone.GUID(target.VehicleSeated) match { case Some(obj: Mountable) => - //boot cadaver from seat internally (vehicle perspective) + //boot cadaver from mount internally (vehicle perspective) obj.PassengerInSeat(target) match { case Some(index) => - obj.Seats(index).Occupant = None + obj.Seats(index).unmount(target) case _ => ; } - //boot cadaver from seat on client + //boot cadaver from mount on client events ! AvatarServiceMessage( nameChannel, AvatarAction.SendResponse( @@ -1046,6 +1049,38 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm suicide() } + def doInteractingWithGantryField( + obj: PlanetSideServerObject, + body: PieceOfEnvironment, + data: Option[OxygenStateTarget] + ): Unit = { + import scala.concurrent.ExecutionContext.Implicits.global + val field = body.asInstanceOf[GantryDenialField] + val zone = player.Zone + (zone.GUID(field.obbasemesh) match { + case Some(pad : OrbitalShuttlePad) => zone.GUID(pad.shuttle) + case _ => None + }) match { + case Some(shuttle: Vehicle) + if shuttle.Flying.contains(ShuttleState.State11.id) || shuttle.Faction != player.Faction => + val (pos, zang) = Vehicles.dismountShuttle(shuttle, field.mountPoint) + shuttle.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + PlayerStateShiftMessage(ShiftState(0, pos, zang, None))) + ) + case Some(_: Vehicle) => + interactionTimer = context.system.scheduler.scheduleOnce( + delay = 250 milliseconds, + self, + InteractWithEnvironment(player, body, None) + ) + case _ => ; + //something configured incorrectly; no need to keep checking + } + } + /** * When out of water, the player is no longer suffocating. * The player does have to endure a recovery period to get back to normal, though. diff --git a/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala b/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala index 4aee68ded..470c1aa1f 100644 --- a/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala @@ -1,35 +1,14 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2021 PSForever package net.psforever.objects.definition -import net.psforever.objects.vehicles.CargoVehicleRestriction +import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.mount.{LargeCargo, MountRestriction, MountableSpaceDefinition} -/** - * The definition for a cargo hold. - */ -class CargoDefinition extends BasicDefinition { - - /** a restriction on the type of exo-suit a person can wear */ - private var vehicleRestriction: CargoVehicleRestriction.Value = CargoVehicleRestriction.Small - - /** the user can escape while the vehicle is moving */ - private var bailable: Boolean = true +class CargoDefinition extends MountableSpaceDefinition[Vehicle] { Name = "cargo" + def occupancy: Int = 1 - def CargoRestriction: CargoVehicleRestriction.Value = { - this.vehicleRestriction - } + var restriction: MountRestriction[Vehicle] = LargeCargo - def CargoRestriction_=(restriction: CargoVehicleRestriction.Value): CargoVehicleRestriction.Value = { - this.vehicleRestriction = restriction - restriction - } - - def Bailable: Boolean = { - this.bailable - } - - def Bailable_=(canBail: Boolean): Boolean = { - this.bailable = canBail - canBail - } + var bailable: Boolean = true } diff --git a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala index 5378b5224..0df091659 100644 --- a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala @@ -89,9 +89,9 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti private var serverGeometry: Any => Geometry3D = GeometryForm.representByPoint() def Geometry: Any => Geometry3D = if (ServerSplashTargetsCentroid) { - serverGeometry - } else { GeometryForm.representByPoint() + } else { + serverGeometry } def Geometry_=(func: Any => Geometry3D): Any => Geometry3D = { diff --git a/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala b/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala deleted file mode 100644 index 7152f873f..000000000 --- a/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.definition - -import net.psforever.objects.vehicles.SeatArmorRestriction - -/** - * The definition for a seat. - */ -class SeatDefinition extends BasicDefinition { - - /** a restriction on the type of exo-suit a person can wear */ - private var armorRestriction: SeatArmorRestriction.Value = SeatArmorRestriction.NoMax - - /** the user can escape while the vehicle is moving */ - private var bailable: Boolean = false - - /** any controlled weapon */ - private var weaponMount: Option[Int] = None - Name = "seat" - - def ArmorRestriction: SeatArmorRestriction.Value = { - this.armorRestriction - } - - def ArmorRestriction_=(restriction: SeatArmorRestriction.Value): SeatArmorRestriction.Value = { - this.armorRestriction = restriction - restriction - } - - /** Determines if the seat can be bailed from while the vehicle is in motion */ - def Bailable: Boolean = { - this.bailable - } - - def Bailable_=(canBail: Boolean): Boolean = { - this.bailable = canBail - canBail - } - - def ControlledWeapon: Option[Int] = { - this.weaponMount - } - - def ControlledWeapon_=(wep: Int): Option[Int] = { - ControlledWeapon_=(Some(wep)) - } - - def ControlledWeapon_=(wep: Option[Int]): Option[Int] = { - this.weaponMount = wep - ControlledWeapon - } -} diff --git a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index d208c2c01..83a41a353 100644 --- a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -4,7 +4,7 @@ package net.psforever.objects.definition import net.psforever.objects.NtuContainerDefinition import net.psforever.objects.definition.converter.VehicleConverter import net.psforever.objects.inventory.InventoryTile -import net.psforever.objects.vehicles.{DestroyedVehicle, UtilityType} +import net.psforever.objects.vehicles.{DestroyedVehicle, MountableWeaponsDefinition, UtilityType} import net.psforever.objects.vital._ import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.resistance.ResistanceProfileMutators @@ -20,19 +20,14 @@ import scala.concurrent.duration._ */ class VehicleDefinition(objectId: Int) extends ObjectDefinition(objectId) + with MountableWeaponsDefinition with VitalityDefinition with NtuContainerDefinition with ResistanceProfileMutators with DamageResistanceModel { /** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */ private var maxShields: Int = 0 - /* key - seat index, value - seat object */ - private val seats: mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]() private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]() - /* key - entry point index, value - seat index */ - private val mountPoints: mutable.HashMap[Int, Int] = mutable.HashMap() - /* key - seat index (where this weapon attaches during object construction), value - the weapon on an EquipmentSlot */ - private val weapons: mutable.HashMap[Int, ToolDefinition] = mutable.HashMap[Int, ToolDefinition]() private var deployment: Boolean = false private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap() private val utilityOffsets: mutable.HashMap[Int, Vector3] = mutable.HashMap() @@ -44,8 +39,16 @@ class VehicleDefinition(objectId: Int) private var trunkLocation: Vector3 = Vector3.Zero private var canCloak: Boolean = false private var canFly: Boolean = false - private var canBeOwned: Boolean = true + /** whether the vehicle gains and/or maintains ownership based on access to the driver seat
+ * `Some(true)` - assign ownership upon the driver mount, maintains ownership after the driver dismounts
+ * `Some(false)` - assign ownership upon the driver mount, becomes unowned after the driver dismounts
+ * `None` - does not assign ownership
+ * Be cautious about using `None` as the client tends to equate the driver seat as the owner's seat for many vehicles + * and breaking from the client's convention either requires additional fields or just doesn't work. + */ + private var canBeOwned: Option[Boolean] = Some(true) private var serverVehicleOverrideSpeeds: (Int, Int) = (0, 0) + var undergoesDecay: Boolean = true private var deconTime: Option[FiniteDuration] = None private var maxCapacitor: Int = 0 private var destroyedModel: Option[DestroyedVehicle.Value] = None @@ -64,15 +67,13 @@ class VehicleDefinition(objectId: Int) MaxShields } - def Seats: mutable.HashMap[Int, SeatDefinition] = seats - def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo - def MountPoints: mutable.HashMap[Int, Int] = mountPoints + def CanBeOwned: Option[Boolean] = canBeOwned - def CanBeOwned: Boolean = canBeOwned + def CanBeOwned_=(ownable: Boolean): Option[Boolean] = CanBeOwned_=(Some(ownable)) - def CanBeOwned_=(ownable: Boolean): Boolean = { + def CanBeOwned_=(ownable: Option[Boolean]): Option[Boolean] = { canBeOwned = ownable CanBeOwned } @@ -91,8 +92,6 @@ class VehicleDefinition(objectId: Int) CanFly } - def Weapons: mutable.HashMap[Int, ToolDefinition] = weapons - def Deployment: Boolean = deployment def Deployment_=(deployable: Boolean): Boolean = { diff --git a/src/main/scala/net/psforever/objects/definition/converter/OrbitalShuttleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/OrbitalShuttleConverter.scala new file mode 100644 index 000000000..69112381f --- /dev/null +++ b/src/main/scala/net/psforever/objects/definition/converter/OrbitalShuttleConverter.scala @@ -0,0 +1,20 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Vehicle +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class OrbitalShuttleConverter extends ObjectCreateConverter[Vehicle]() { + override def ConstructorData(obj: Vehicle): Try[OrbitalShuttleData] = { +// if (obj.MountedIn.nonEmpty) { +// Success(OrbitalShuttleData(obj.Faction, None)) +// } else { + Success(OrbitalShuttleData(obj.Faction, Some(PlacementData(obj.Position, obj.Orientation)))) +// } + } + + override def DetailedConstructorData(obj: Vehicle): Try[OrbitalShuttleData] = + Failure(new Exception("OrbitalShuttleConverter should not be used to generate detailed DroppodData (nothing should)")) +} diff --git a/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala index ced03f488..a7a23f7b5 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala @@ -2,7 +2,7 @@ package net.psforever.objects.definition.converter import net.psforever.objects.Player -import net.psforever.objects.vehicles.Seat +import net.psforever.objects.serverobject.mount.Seat import net.psforever.packet.game.objectcreate.{InventoryItemData, ObjectClass, PlayerData, VehicleData} object SeatConverter { @@ -16,14 +16,14 @@ object SeatConverter { ) } - //TODO do not use for now; causes seat access permission issues with many passengers; may not mesh with workflows; GUID requirements + //TODO do not use for now; causes mount access permission issues with many passengers; may not mesh with workflows; GUID requirements def MakeSeats(seats: Map[Int, Seat], initialOffset: Long): List[InventoryItemData.InventoryItem] = { var offset = initialOffset seats .filter({ case (_, seat) => seat.isOccupied }) .map({ - case (index, seat) => - val player = seat.Occupant.get + case (index: Int, seat: Seat) => + val player = seat.occupant.get val entry = InventoryItemData(ObjectClass.avatar, player.GUID, index, SeatConverter.MakeSeat(player, offset)) offset += entry.bitsize entry diff --git a/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala index f30cafca9..ed972465f 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala @@ -14,7 +14,7 @@ class VariantVehicleConverter extends VehicleConverter { */ Some( VariantVehicleData( - if (obj.Definition.CanFly && obj.Flying) 7 else 0 + if (obj.Definition.CanFly && obj.isFlying) 7 else 0 ) ) } diff --git a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 303fd4bf6..596bf5c8e 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -76,7 +76,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = { val offset: Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) - obj.Seats(0).Occupant match { + obj.Seats(0).occupant match { case Some(player) => List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset))) case None => diff --git a/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala b/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala index 0bf64cf7b..ce74af5c3 100644 --- a/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala +++ b/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala @@ -13,7 +13,7 @@ object EquipmentSize extends Enumeration { VehicleWeapon, //vehicle-mounted weapons BaseTurretWeapon, //common phalanx cannons, and cavern turrets BFRArmWeapon, //duel arm weapons for bfr - BFRGunnerWeapon, //gunner seat for bfr + BFRGunnerWeapon, //gunner mount for bfr Inventory //reserved = Value diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala index 1aeb52644..6a028144c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala @@ -118,10 +118,10 @@ trait AggravatedBehavior { ): AggravatedBehavior.Entry = { val cause = data.cause val aggravatedDamageInfo = DamageInteraction( - AggravatedDamage.burning(cause.resolution), target, + data.hitPos, cause, - data.hitPos + AggravatedDamage.burning(cause.resolution) ) val entry = AggravatedBehavior.Entry(id, effect, retime, aggravatedDamageInfo, powerOffset) entryIdToEntry += id -> entry diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala index 3e5150ce5..1ad0c43d0 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala @@ -36,10 +36,7 @@ object DamageableMountable { ): Unit = { val zone = target.Zone val events = zone.AvatarEvents - val occupants = target.Seats.values.collect { - case seat if seat.isOccupied && seat.Occupant.get.isAlive => - seat.Occupant.get - } + val occupants = target.Seats.values.toSeq.flatMap { seat => seat.occupants.filter(_.isAlive) } ((cause.adversarial match { case Some(adversarial) => Some(adversarial.attacker) case None => None @@ -80,10 +77,10 @@ object DamageableMountable { val interaction = cause.interaction target.Seats.values .filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive + seat.isOccupied && seat.occupant.get.isAlive }) .foreach(seat => { - val tplayer = seat.Occupant.get + val tplayer = seat.occupant.get //tplayer.History(cause) tplayer.Actor ! Player.Die( DamageInteraction(interaction.resolution, SourceEntry(tplayer), interaction.cause, interaction.hitPos) diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index 694010592..44dfbbc6a 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -145,7 +145,7 @@ trait DamageableVehicle if (aggravated) { val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(totalDamage, Vector3.Zero)) obj.Seats.values - .collect { case seat if seat.Occupant.nonEmpty => seat.Occupant.get.Name } + .map { case seat if seat.occupant.nonEmpty => seat.occupant.get.Name } .foreach { channel => events ! VehicleServiceMessage(channel, msg) } @@ -158,7 +158,7 @@ trait DamageableVehicle } //alert cargo occupants to damage source obj.CargoHolds.values.foreach(hold => { - hold.Occupant match { + hold.occupant match { case Some(cargo) => cargo.Actor ! DamageableVehicle.Damage(cause, totalDamage) case None => ; @@ -198,7 +198,7 @@ trait DamageableVehicle DamageableMountable.DestructionAwareness(obj, cause) //cargo vehicles die with us obj.CargoHolds.values.foreach(hold => { - hold.Occupant match { + hold.occupant match { case Some(cargo) => cargo.Actor ! DamageableVehicle.Destruction(cause) case None => ; diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala index 4b8400077..6ba61f483 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala @@ -73,7 +73,7 @@ trait DamageableWeaponTurret if (aggravated) { val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(damageToHealth, Vector3.Zero)) obj.Seats.values - .collect { case seat if seat.Occupant.nonEmpty => seat.Occupant.get.Name } + .collect { case seat if seat.occupant.nonEmpty => seat.occupant.get.Name } .foreach { channel => events ! VehicleServiceMessage(channel, msg) } diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala b/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala index b10574bf5..e6c34b847 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.doors import net.psforever.objects.Player +import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.UseItemMessage @@ -65,6 +66,14 @@ object Door { */ final case class NoEvent() extends Exchange + type LockingMechanismLogic = (PlanetSideServerObject, Door) => Boolean + + final case class UpdateMechanism(mechanism: LockingMechanismLogic) extends Exchange + + case object Lock extends Exchange + + case object Unlock extends Exchange + /** * Overloaded constructor. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields @@ -101,12 +110,25 @@ object Door { * @return the `Door` object */ def Constructor(pos: Vector3)(id: Int, context: ActorContext): Door = { - import akka.actor.Props import net.psforever.objects.GlobalDefinitions + Constructor(pos, GlobalDefinitions.door)(id, context) + } - val obj = Door(GlobalDefinitions.door) + /** + * Instantiate and configure a `Door` object that has knowledge of both its position and outwards-facing direction. + * The assumption is that this door will be paired with an IFF Lock, thus, has conditions for opening. + * @param pos the position of the door + * @param ddef the definition for this specific type of door + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `Door` object + */ + def Constructor(pos: Vector3, ddef: DoorDefinition)(id: Int, context: ActorContext): Door = { + import akka.actor.Props + + val obj = Door(ddef) obj.Position = pos - obj.Actor = context.actorOf(Props(classOf[DoorControl], obj), s"${GlobalDefinitions.door.Name}_$id") + obj.Actor = context.actorOf(Props(classOf[DoorControl], obj), s"${ddef.Name}_$id") obj } } diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala index 10fa58b8a..a13c1b557 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -2,13 +2,12 @@ package net.psforever.objects.serverobject.doors import net.psforever.objects.Player -import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.locks.IFFLock -import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} +import net.psforever.objects.serverobject.structures.PoweredAmenityControl import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} -import net.psforever.types.{PlanetSideEmpire, Vector3} /** * An `Actor` that handles messages being dispatched to a specific `Door`. @@ -18,44 +17,44 @@ class DoorControl(door: Door) extends PoweredAmenityControl with FactionAffinityBehavior.Check { def FactionObject: FactionAffinity = door + var isLocked: Boolean = false + var lockingMechanism: Door.LockingMechanismLogic = DoorControl.alwaysOpen val commonBehavior: Receive = checkBehavior + .orElse { + case Door.Lock => + isLocked = true + if (door.isOpen) { + val zone = door.Zone + door.Open = None + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.DoorSlamsShut(door)) + } + + case Door.Unlock => + isLocked = false + + case Door.UpdateMechanism(logic) => + lockingMechanism = logic + } def poweredStateLogic: Receive = commonBehavior .orElse { case CommonMessages.Use(player, _) => - val zone = door.Zone - val doorGUID = door.GUID - if ( - player.Faction == door.Faction || (zone.GUID(zone.map.doorToLock.getOrElse(doorGUID.guid, 0)) match { - case Some(lock: IFFLock) => - val owner = lock.Owner.asInstanceOf[Building] - val playerIsOnInside = Vector3.ScalarProjection(lock.Outwards, player.Position - door.Position) < 0f - /* - If an IFF lock exists and - the IFF lock faction doesn't match the current player and - one of the following conditions are met: - 1. player is on the inside of the door (determined by the lock orientation) - 2. lock is hacked - 3. facility capture terminal has been hacked - 4. base is neutral - ... open the door. - */ - playerIsOnInside || lock.HackedBy.isDefined || owner.CaptureTerminalIsHacked || lock.Faction == PlanetSideEmpire.NEUTRAL - case _ => true // no linked IFF lock, just try open the door - }) - ) { + if (lockingMechanism(player, door) && !isLocked) { openDoor(player) } + case IFFLock.DoorOpenResponse(target: Player) if !isLocked => + openDoor(target) + case _ => ; } def unpoweredStateLogic: Receive = { commonBehavior .orElse { - case CommonMessages.Use(player, _) => + case CommonMessages.Use(player, _) if !isLocked => //without power, the door opens freely openDoor(player) @@ -88,3 +87,7 @@ class DoorControl(door: Door) override def powerTurnOnCallback() : Unit = { } } + +object DoorControl { + def alwaysOpen(obj: PlanetSideServerObject, door: Door): Boolean = true +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala index 105acb1ee..aa9d3274a 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala @@ -2,9 +2,9 @@ package net.psforever.objects.serverobject.environment import enumeratum.{Enum, EnumEntry} -import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.vital.Vitality -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideGUID, Vector3} /** * The representation of a feature of the game world that is not a formal game object, @@ -76,6 +76,17 @@ object EnvironmentAttribute extends Enum[EnvironmentTrait] { } } } + + case object GantryDenialField + extends EnvironmentTrait { + /** only interact with living player characters */ + def canInteractWith(obj: PlanetSideGameObject): Boolean = { + obj match { + case p: Player => p.isAlive + case _ => false + } + } + } } /** @@ -123,6 +134,14 @@ object Pool { Pool(attribute, DeepSquare(altitude, north, east, south, west)) } +final case class GantryDenialField( + obbasemesh: PlanetSideGUID, + mountPoint: Int, + collision: EnvironmentCollision + ) extends PieceOfEnvironment { + def attribute = EnvironmentAttribute.GantryDenialField +} + object PieceOfEnvironment { /** * Did the test point move into or leave the bounds of the represented environment since its previous test? diff --git a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala index ab13ad787..12953d835 100644 --- a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala +++ b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala @@ -1,6 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.locks +import akka.actor.ActorRef +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.TriggeredSound @@ -48,6 +51,14 @@ class IFFLock(private val idef: IFFLockDefinition) extends Amenity with Hackable } object IFFLock { + final case class DoorOpenRequest(requestee: PlanetSideServerObject, door: Door, replyTo: ActorRef) + + final case class DoorOpenResponse(requestee: PlanetSideServerObject) + + def testLock(lock: IFFLock)(target: PlanetSideServerObject, door: Door): Boolean = { + lock.Actor ! IFFLock.DoorOpenRequest(target, door, door.Actor) + false + } /** * Overloaded constructor. diff --git a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala index 59d665ded..9d0f79024 100644 --- a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -6,6 +6,8 @@ import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} +import net.psforever.objects.serverobject.structures.Building +import net.psforever.types.{PlanetSideEmpire, Vector3} /** * An `Actor` that handles messages being dispatched to a specific `IFFLock`. @@ -44,6 +46,27 @@ class IFFLockControl(lock: IFFLock) log.warn(s"Player - Faction=${player.Faction}") } + case IFFLock.DoorOpenRequest(target, door, replyTo) => + val owner = lock.Owner.asInstanceOf[Building] + /* + If one of the following conditions are met: + 1. target and door have same faction affinity + 2. lock or lock owner is neutral + 3. lock is hacked + 4. facility capture terminal (owner is a building) has been hacked + 5. requestee is on the inside of the door (determined by the lock orientation) + ... open the door. + */ + if ( + lock.Faction == target.Faction || + lock.Faction == PlanetSideEmpire.NEUTRAL || owner.Faction == PlanetSideEmpire.NEUTRAL || + lock.HackedBy.isDefined || + owner.CaptureTerminalIsHacked || + Vector3.ScalarProjection(lock.Outwards, target.Position - door.Position) < 0f + ) { + replyTo ! IFFLock.DoorOpenResponse(target) + } + case _ => ; //no default message } } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala new file mode 100644 index 000000000..be674fcb0 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala @@ -0,0 +1,45 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.types.ExoSuitType + +trait MountRestriction[A] { + def test(target: A): Boolean +} + +case object MaxOnly extends MountRestriction[Player] { + def test(target: Player): Boolean = target.ExoSuit == ExoSuitType.MAX +} + +case object NoMax extends MountRestriction[Player] { + def test(target: Player): Boolean = target.ExoSuit != ExoSuitType.MAX +} + +case object NoReinforcedOrMax extends MountRestriction[Player] { + def test(target: Player): Boolean = target.ExoSuit != ExoSuitType.Reinforced && target.ExoSuit != ExoSuitType.MAX +} + +case object Unrestricted extends MountRestriction[Player] { + def test(target: Player): Boolean = true +} + +case object SmallCargo extends MountRestriction[Vehicle] { + def test(target: Vehicle): Boolean = { + target.Definition == GlobalDefinitions.ant || + target.Definition == GlobalDefinitions.quadassault || + target.Definition == GlobalDefinitions.quadstealth || + target.Definition == GlobalDefinitions.fury || + target.Definition == GlobalDefinitions.switchblade || + target.Definition == GlobalDefinitions.two_man_assault_buggy || + target.Definition == GlobalDefinitions.skyguard || + target.Definition == GlobalDefinitions.twomanheavybuggy || + target.Definition == GlobalDefinitions.twomanhoverbuggy || + target.Definition == GlobalDefinitions.threemanheavybuggy || + target.Definition == GlobalDefinitions.lightning + } +} + +case object LargeCargo extends MountRestriction[Vehicle] { + def test(target : Vehicle) : Boolean = !target.Definition.CanFly +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala index abb6e7912..16211210e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala @@ -3,7 +3,8 @@ package net.psforever.objects.serverobject.mount import akka.actor.ActorRef import net.psforever.objects.Player -import net.psforever.objects.vehicles.Seat + +import scala.annotation.tailrec /** * A `Trait` common to all game objects that permit players to @@ -12,38 +13,63 @@ import net.psforever.objects.vehicles.Seat * @see `Seat` */ trait Mountable { + protected var seats: Map[Int, Seat] = Map.empty /** - * Retrieve a mapping of each seat from its internal index. - * @return the mapping of index to seat + * Retrieve a mapping of each mount from its internal index. + * @return the mapping of index to mount */ - def Seats: Map[Int, Seat] + def Seats: Map[Int, Seat] = seats /** - * Given a seat's index position, retrieve the internal `Seat` object. - * @return the specific seat + * Given a mount's index position, retrieve the internal `Seat` object. + * @return the specific mount */ - def Seat(seatNum: Int): Option[Seat] + def Seat(seatNumber: Int): Option[Seat] = { + if (seatNumber >= 0 && seatNumber < seats.size) { + seats.get(seatNumber) + } else { + None + } + } /** - * Retrieve a mapping of each seat from its mount point index. - * @return the mapping of mount point to seat + * Retrieve a mapping of each mount from its mount point index. + * @return the mapping of mount point to mount */ - def MountPoints: Map[Int, Int] + def MountPoints: Map[Int, MountInfo] = Definition.MountPoints.toMap /** - * Given a mount point index, return the associated seat index. - * @param mount the mount point - * @return the seat index + * Given a mount point index, return the associated mount index. + * @param mountPoint the mount point + * @return the mount index */ - def GetSeatFromMountPoint(mount: Int): Option[Int] + def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { + MountPoints.get(mountPoint) match { + case Some(mp) => Some(mp.seatIndex) + case _ => None + } + } /** * Given a player, determine if that player is seated. * @param user the player - * @return the seat index + * @return the mount index */ - def PassengerInSeat(user: Player): Option[Int] + def PassengerInSeat(user: Player): Option[Int] = recursivePassengerInSeat(seats.iterator, user) + + @tailrec private def recursivePassengerInSeat(iter: Iterator[(Int, Seat)], player: Player): Option[Int] = { + if (!iter.hasNext) { + None + } else { + val (seatNumber, seat) = iter.next() + if (seat.occupant.contains(player)) { + Some(seatNumber) + } else { + recursivePassengerInSeat(iter, player) + } + } + } /** * A reference to an `Actor` that governs the logic of the object to accept `Mountable` messages. @@ -53,6 +79,8 @@ trait Mountable { * @return the internal `ActorRef` */ def Actor: ActorRef //TODO can we enforce this desired association to MountableControl? + + def Definition: MountableDefinition } object Mountable { @@ -60,10 +88,15 @@ object Mountable { /** * Message used by the player to indicate the desire to board a `Mountable` object. * @param player the player who sent this request message + * @param mount_point the mount index + */ + final case class TryMount(player: Player, mount_point: Int) + + /** + * Message used by the player to indicate the desire to escape a `Mountable` object. + * @param player the player who sent this request message * @param seat_num the seat index */ - final case class TryMount(player: Player, seat_num: Int) - final case class TryDismount(player: Player, seat_num: Int) /** @@ -82,17 +115,17 @@ object Mountable { * Message sent in response to the player succeeding to access a `Mountable` object. * The player should be seated at the given index. * @param obj the `Mountable` object - * @param seat_num the seat index + * @param mount_point the mount index */ - final case class CanMount(obj: Mountable, seat_num: Int) extends Exchange + final case class CanMount(obj: Mountable, seat_number: Int, mount_point: Int) extends Exchange /** * Message sent in response to the player failing to access a `Mountable` object. * The player would have been be seated at the given index. * @param obj the `Mountable` object - * @param seat_num the seat index + * @param mount_point the mount index */ - final case class CanNotMount(obj: Mountable, seat_num: Int) extends Exchange + final case class CanNotMount(obj: Mountable, mount_point: Int) extends Exchange /** * Message sent in response to the player succeeding to disembark a `Mountable` object. @@ -100,7 +133,7 @@ object Mountable { * @param obj the `Mountable` object * @param seat_num the seat index */ - final case class CanDismount(obj: Mountable, seat_num: Int) extends Exchange + final case class CanDismount(obj: Mountable, seat_num: Int, mount_point: Int) extends Exchange /** * Message sent in response to the player failing to disembark a `Mountable` object. diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala index f71d1f700..9d30a9990 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala @@ -2,15 +2,33 @@ package net.psforever.objects.serverobject.mount import akka.actor.Actor -import net.psforever.objects.{Player, Vehicle} -import net.psforever.objects.entity.{Identifiable, WorldEntity} +import net.psforever.objects.Player +import net.psforever.objects.entity.WorldEntity import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.turret.WeaponTurret -import net.psforever.types.DriveState -object MountableBehavior { +import scala.collection.mutable + +trait MountableBehavior { + _ : Actor => + def MountableObject: PlanetSideServerObject with Mountable + + /** retain the mount point that was used by this occupant to mount */ + val usedMountPoint: mutable.HashMap[String, Int] = mutable.HashMap() + + def getUsedMountPoint(playerName: String, seatNumber: Int): Int = { + usedMountPoint + .remove(playerName) + .getOrElse { + MountableObject + .Definition + .MountPoints + .find { case (_, mp) => mp.seatIndex == seatNumber } match { + case Some((mount, _)) => mount + case None => -1 + } + } + } /** * The logic governing `Mountable` objects that use the `TryMount` message. @@ -18,54 +36,40 @@ object MountableBehavior { * @see `Seat` * @see `Mountable` */ - trait Mount { - _: Actor => - def MountableObject: PlanetSideServerObject with Mountable with FactionAffinity - - val mountBehavior: Receive = { - case Mountable.TryMount(user, seat_num) => - val obj = MountableObject - if (MountTest(MountableObject, seat_num, user)) { + val mountBehavior: Receive = { + case Mountable.TryMount(user, mount_point) => + val obj = MountableObject + obj.GetSeatFromMountPoint(mount_point) match { + case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) => user.VehicleSeated = obj.GUID - sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) - } else { - sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) - } - } - - protected def MountTest(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = { - (player.Faction == obj.Faction || - (obj match { - case o: Hackable => o.HackedBy.isDefined - case _ => false - })) && - !obj.Destroyed && - (obj.Seats.get(seatNumber) match { - case Some(seat) => (seat.Occupant = player).contains(player) - case _ => false - }) - } + usedMountPoint.put(user.Name, mount_point) + sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point)) + case _ => + sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point)) + } } - trait TurretMount extends Mount { - _: Actor => + protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player + ): Boolean = { + (player.Faction == obj.Faction || + (obj match { + case o : Hackable => o.HackedBy.isDefined + case _ => false + })) && + !obj.Destroyed + } - override protected def MountTest( - obj: PlanetSideServerObject with Mountable, - seatNumber: Int, - player: Player - ): Boolean = { - obj match { - case wep: WeaponTurret => - (!wep.Definition.FactionLocked || player.Faction == obj.Faction) && - !obj.Destroyed && - (obj.Seats.get(seatNumber) match { - case Some(seat) => (seat.Occupant = player).contains(player) - case _ => false - }) - case _ => - super.MountTest(obj, seatNumber, player) - } + private def tryMount( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player + ): Boolean = { + obj.Seat(seatNumber) match { + case Some(seat) => seat.mount(player).contains(player) + case _ => false } } @@ -75,29 +79,41 @@ object MountableBehavior { * @see `Seat` * @see `Mountable` */ - trait Dismount { - this: Actor => + val dismountBehavior: Receive = { + case Mountable.TryDismount(user, seat_number) => + val obj = MountableObject + if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user)) { + user.VehicleSeated = None + sender() ! Mountable.MountMessages( + user, + Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number)) + ) + } + else { + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_number)) + } + } - def MountableObject: Mountable with Identifiable with WorldEntity with FactionAffinity + protected def dismountTest( + obj: Mountable with WorldEntity, + seatNumber: Int, + user: Player + ): Boolean = { + obj.PassengerInSeat(user).contains(seatNumber) && + (obj.Seats.get(seatNumber) match { + case Some(seat) => seat.bailable || !obj.isMoving(test = 1) + case _ => false + }) + } - val dismountBehavior: Receive = { - case Mountable.TryDismount(user, seat_num) => - val obj = MountableObject - obj.Seat(seat_num) match { - case Some(seat) => - if ( - seat.Bailable || !obj.isMoving(1) || (obj - .isInstanceOf[Vehicle] && obj.asInstanceOf[Vehicle].DeploymentState == DriveState.Deployed) - ) { - seat.Occupant = None - user.VehicleSeated = None - sender() ! Mountable.MountMessages(user, Mountable.CanDismount(obj, seat_num)) - } else { - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_num)) - } - case None => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_num)) - } + private def tryDismount( + obj: Mountable, + seatNumber: Int, + user: Player + ): Boolean = { + obj.Seats.get(seatNumber) match { + case Some(seat) => seat.unmount(user).isEmpty + case _ => false } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableDefinition.scala new file mode 100644 index 000000000..e72641f34 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableDefinition.scala @@ -0,0 +1,23 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.types.Vector3 + +import scala.collection.mutable + +final case class MountInfo(seatIndex: Int, positionOffset: Vector3) + +object MountInfo { + def apply(seatIndex: Int): MountInfo = MountInfo(seatIndex, Vector3.Zero) +} + +trait MountableDefinition { + /* key - mount index, value - mount object */ + private val seats: mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]() + /* key - entry point index, value - mount index */ + private val mountPoints: mutable.HashMap[Int, MountInfo] = mutable.HashMap() + + def Seats: mutable.HashMap[Int, SeatDefinition] = seats + + def MountPoints: mutable.HashMap[Int, MountInfo] = mountPoints +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala new file mode 100644 index 000000000..25c046ba7 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala @@ -0,0 +1,104 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +trait MountableSpace[A] { + private var _occupant: Option[A] = None + + /** + * A single mounted entity. + * @return one mounted entity at most, or `None` + */ + def occupant: Option[A] = _occupant + + /** + * A collection of any mounted entity. + * Useful for compiling all seated users using `flatMap`. + * @return all mounted entities + */ + def occupants: List[A] = _occupant.toList + + /** + * Is anything be seated? + * Do not use this method as a test for "availability". + */ + def isOccupied: Boolean = _occupant.nonEmpty + + /** + * Can something be mounted? + * Use this method as a test for "availability". + */ + def canBeOccupied: Boolean = _occupant.isEmpty + + /** + * Is this specific entity currently mounted? + */ + def isOccupiedBy(target: A): Boolean = _occupant.contains(target) + + /** + * Is this specific entity allowed to be mounted in this space? + * Utiltizes restriction tests, but not "availability" tests. + * @see `MountableDefinition[A].restriction` + */ + def canBeOccupiedBy(target: A): Boolean = definition.restriction.test(target) + + /** + * Attempt to mount the target entity in this space. + */ + def mount(target: A): Option[A] = mount(Some(target)) + + /** + * Attempt to mount the target entity in this space. + */ + def mount(target: Option[A]): Option[A] = { + target match { + case Some(p) if testToMount(p) => + _occupant = target + target + case _ => + occupant + } + } + + /** + * Tests whether the target is allowed to be mounted. + * @see `MountableSpace[A].canBeOccupiedBy(A)` + */ + protected def testToMount(target: A): Boolean = canBeOccupied && canBeOccupiedBy(target) + + /** + * Attempt to dismount the target entity from this space. + */ + def unmount(target: A): Option[A] = unmount(Some(target)) + + /** + * Attempt to dismount the target entity from this space. + */ + def unmount(target: Option[A]): Option[A] = { + target match { + case Some(p) if testToUnmount(p) => + _occupant = None + None + case _ => + occupant + } + } + + /** + * Tests whether the target is capable of being unmounted from this place. + * @see `MountableSpace[A].isOccupiedBy(A)` + */ + protected def testToUnmount(target: A): Boolean = isOccupiedBy(target) + + /** + * Does this mountable space count as being "bailable", + * a condition whereupon it can be unmounted under duress? + * The conditions of the duress do not matter at the moment; + * this is only a test of possibility. + */ + def bailable: Boolean = definition.bailable + + /** + * The information that establishes the underlying characteristics of this mountable space. + */ + def definition: MountableSpaceDefinition[A] +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpaceDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpaceDefinition.scala new file mode 100644 index 000000000..2b199d0f2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpaceDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.definition.BasicDefinition + +trait MountableSpaceDefinition[A] + extends BasicDefinition { + def occupancy: Int + + def restriction: MountRestriction[A] + + def bailable: Boolean +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/Seat.scala b/src/main/scala/net/psforever/objects/serverobject/mount/Seat.scala new file mode 100644 index 000000000..23b5239b5 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/Seat.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.Player + +class Seat(private val sdef: SeatDefinition) extends MountableSpace[Player] { + override protected def testToMount(target: Player): Boolean = target.VehicleSeated.isEmpty && super.testToMount(target) + + def definition: SeatDefinition = sdef +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/SeatDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/mount/SeatDefinition.scala new file mode 100644 index 000000000..dfcd9ed84 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/SeatDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.Player + +class SeatDefinition extends MountableSpaceDefinition[Player] { + Name = "mount" + var occupancy: Int = 1 + + var restriction: MountRestriction[Player] = NoMax + + var bailable: Boolean = false +} diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala index 5b5ac1737..8d3dfdb9b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala @@ -87,7 +87,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) /* When the vehicle is spawned and added to the pad, it will "occupy" the pad and block it from further action. - Normally, the player who wanted to spawn the vehicle will be automatically put into the driver seat. + Normally, the player who wanted to spawn the vehicle will be automatically put into the driver mount. If this is blocked, the vehicle will idle on the pad and must be moved far enough away from the point of origin. During this time, a periodic message about the spawn pad being blocked will be broadcast to all current customers in the order queue. @@ -220,8 +220,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) */ def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnControl.Order]): Unit = { val user = blockedOrder.vehicle - .Seats(0) - .Occupant + .Seats(0).occupant .orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner)) .orElse(pad.Zone.GUID(blockedOrder.DriverGUID)) val relevantRecipients = user match { diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala index 61db390dc..f3c9ceeae 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala @@ -75,7 +75,7 @@ object VehicleSpawnPad { final case class ResetSpawnPad(pad: VehicleSpawnPad) /** - * Message that acts as callback to the driver that the process of sitting in the driver seat will be initiated soon. + * Message that acts as callback to the driver that the process of sitting in the driver mount will be initiated soon. * This information should only be communicated to the driver's client only. * @param driver_name the person who will drive the vehicle * @param vehicle the vehicle being spawned @@ -84,7 +84,7 @@ object VehicleSpawnPad { final case class StartPlayerSeatedInVehicle(driver_name: String, vehicle: Vehicle, pad: VehicleSpawnPad) /** - * Message that acts as callback to the driver that the process of sitting in the driver seat should be finished. + * Message that acts as callback to the driver that the process of sitting in the driver mount should be finished. * This information should only be communicated to the driver's client only. * @param driver_name the person who will drive the vehicle * @param vehicle the vehicle being spawned diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala index 5b3c39b0b..2667d785b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala @@ -14,7 +14,7 @@ import scala.concurrent.duration._ *
* This object is the first link in the process chain that spawns the ordered vehicle. * It is devoted to causing the prospective driver to become hidden during the first part of the process - * with the goal of appearing to be "teleported" into the driver seat. + * with the goal of appearing to be "teleported" into the driver mount. * It has failure cases should the driver be in an incorrect state. * @param pad the `VehicleSpawnPad` object being governed */ diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala index 98e24cd04..d589bf0df 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala @@ -23,7 +23,7 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont def LogId = "-lifter" val seatDriver = - context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-seat") + context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-mount") def receive: Receive = { case order @ VehicleSpawnControl.Order(_, vehicle) => diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala index 699165304..4235cf554 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -13,11 +13,11 @@ import scala.concurrent.duration._ * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
*
- * This object forces the prospective driver to take the driver seat. + * This object forces the prospective driver to take the driver mount. * Multiple separate but sequentially significant steps occur within the scope of this object. * First, this step waits for the vehicle to be completely ready to accept the driver. - * Second, this step triggers the player to actually be moved into the driver seat. - * Finally, this step waits until the driver is properly in the driver seat. + * Second, this step triggers the player to actually be moved into the driver mount. + * Finally, this step waits until the driver is properly in the driver mount. * It has failure cases should the driver or the vehicle be in an incorrect state. * @see `ZonePopulationActor` * @param pad the `VehicleSpawnPad` object being governed diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttle.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttle.scala new file mode 100644 index 000000000..3725fbb53 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttle.scala @@ -0,0 +1,85 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import net.psforever.objects.Vehicle +import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.serverobject.mount.Seat +import net.psforever.objects.vehicles.AccessPermissionGroup + +/** + * The high altitude rapid transport (HART) orbital shuttle is a special vehicle + * that is paired with a formal building `Amenity` called the orbital shuttle pad (`obbasemesh`) + * and is only found in the HART buildings (`orbital_building_`{faction}) of a given faction's sanctuary zone.
+ *
+ * It has no pilot and can not be piloted. + * Unlike other vehicles, it has the potential for a very sizeable passenger capacity. + * Despite this, it is intended to start with a single mount. + * That one mount should contain the information needed to create a given number of spontaneous passenger mount points. + * Whenever a valid user would try to find a mount, and there are no mounts available, + * and the total number of created mounts has not yet exceeded the limits set by the original mount's designation, + * then a completely new mount can be created and the user attached. + * All spontaneous mounts have the same properties as the original mount. + * @param sdef the vehicle's definition entry + */ +class OrbitalShuttle(sdef: VehicleDefinition) extends Vehicle(sdef) { + /** + * Either locate a place for a passenger to mount, + * or designate a spontaneous mount point to handle a new passenger. + * The only time there is no more space is when the no new spontaneous seats can be counted. + * @param mountPoint the mount point + * @return the mount index + */ + override def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { + super.GetSeatFromMountPoint(mountPoint) match { + case Some(0) => + seats.find { case (_, seat) => !seat.isOccupied } match { + case Some((seatNumber, _)) => Some(seatNumber) + case None if seats.size < seats(0).definition.occupancy => Some(seats.size) + case _ => None + } + case _ => + None + } + } + + /** + * Either locate a place for a passenger to mount, + * or create a spontaneous mount point to handle the new passenger. + * The only time there is no more space is when the no new spontaneous seats can be created. + * This new seat becomes "real" and will continue to exist after being dismounted. + * @param seatNumber the index of a mount point + * @return the specific mount + */ + override def Seat(seatNumber: Int): Option[Seat] = { + val sdef = seats(0).definition + super.Seat(seatNumber) match { + case out @ Some(_) => + out + case None if seatNumber == seats.size && seatNumber < sdef.occupancy => + val newSeat = new Seat(sdef) + seats = seats ++ Map(seatNumber -> newSeat) + Some(newSeat) + case _ => + None + } + } + + /** + * All players mounted in the shuttle are passengers only. No driver. No gunners. + * Even if it does not exist yet, as long as it has the potential to be created, + * discuss the next seat that would be created as if it already exists. + * @param seatNumber the index of a mount point + * @return `Passenger` permissions + */ + override def SeatPermissionGroup(seatNumber : Int) : Option[AccessPermissionGroup.Value] = { + Seats.get(seatNumber) match { + case Some(_) => + Some(AccessPermissionGroup.Passenger) + case None + if seats.size == seatNumber && Seats.values.exists { _.definition.occupancy > seats.size } => + Some(AccessPermissionGroup.Passenger) + case _ => + None + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePad.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePad.scala new file mode 100644 index 000000000..79d4e5e8f --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePad.scala @@ -0,0 +1,71 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import akka.actor.ActorRef +import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.structures.{Amenity, AmenityDefinition} +import net.psforever.types.PlanetSideGUID + +/** + * The orbital shuttle pad which is the primary component of the high altitude rapid transport (HART) system.
+ *
+ * The orbital shuttle pad is a type of flat called an `obbasemesh`. + * The shuttle component of the HART casually perches on top of the pad and + * adjusts its states to control animation and passenger access. + * The shuttle that is visible to the player and flies in and out of the zone is actually a hologram + * of the real shuttle that is an invisible, intangible vehicle + * forever stationary on top of the building. + * @param spDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class OrbitalShuttlePad(spDef: AmenityDefinition) extends Amenity { + private var _shuttle: Option[PlanetSideGUID] = None + + def shuttle: Option[PlanetSideGUID] = _shuttle + + def shuttle_=(orbitalShuttle: Vehicle): Option[PlanetSideGUID] = { + _shuttle = _shuttle.orElse(Some(orbitalShuttle.GUID)) + _shuttle + } + + def Definition: AmenityDefinition = spDef +} + +object OrbitalShuttlePad { + final case class GetShuttle(giveTo: ActorRef) + + final case class GiveShuttle(shuttle: Vehicle) + + /** + * Overloaded constructor. + * @param spDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @return an `OrbitalShuttlePad` object + */ + def apply(spDef: AmenityDefinition): OrbitalShuttlePad = { + new OrbitalShuttlePad(spDef) + } + + import akka.actor.ActorContext + import net.psforever.types.Vector3 + + /** + * Instantiate and configure an `OrbitalShuttlePad` object + * @param pdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @param pos the position (used to determine spawn point) + * @param orient the orientation (used to indicate spawn direction) + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `OrbitalShuttlePad` object + */ + def Constructor(pos: Vector3, pdef: AmenityDefinition, orient: Vector3)( + id: Int, + context: ActorContext + ): OrbitalShuttlePad = { + import akka.actor.Props + + val obj = OrbitalShuttlePad(pdef) + obj.Position = pos + obj.Orientation = orient + obj.Actor = context.actorOf(Props(classOf[OrbitalShuttlePadControl], obj), s"${obj.Definition.Name}_$id") + obj + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePadControl.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePadControl.scala new file mode 100644 index 000000000..83f218cb4 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePadControl.scala @@ -0,0 +1,203 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import akka.actor.{Actor, ActorRef} +import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} +import net.psforever.objects.{Player, Vehicle} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.ChatMsg +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.hart.{HartTimer, HartTimerActions} +import net.psforever.services.{Service, ServiceManager} +import net.psforever.types.ChatMessageType + +import scala.util.Success + +/** + * An `Actor` that handles messages being dispatched to a specific `OrbitalShuttlePad`.
+ *
+ * For the purposes of maintaining a close relationship + * with the rest of the high altitude rapid transport (HART) system's components, + * this control agency also locally creates the vehicle that will the shuttle when it starts up. + * The shuttle should be treated like a supporting object to the zone + * that exists within the normal vehicle pipeline. + * @see `ShuttleState` + * @see `ShuttleTimer` + * @see `HartService` + * @param pad the `OrbitalShuttlePad` object being governed + */ +class OrbitalShuttlePadControl(pad: OrbitalShuttlePad) extends Actor { + /** the doors that allow would be passengers to access the shuttle boarding gantries + * (actually, a hallway with a teleport); + * the target doors are of a specific type that flag their purpose - "gr_door_mb_orb" + */ + var managedDoors: List[Door] = Nil + var shuttle: Vehicle = _ + + def receive: Receive = startUp + + /** the HART system is active and ready to handle state changes */ + val taxiing: Receive = { + case OrbitalShuttlePad.GetShuttle(to) => + to ! OrbitalShuttlePad.GiveShuttle(shuttle) + + case HartTimer.LockDoors => + managedDoors.foreach { door => + door.Actor ! Door.UpdateMechanism(OrbitalShuttlePadControl.lockedWaitingForShuttle) + val zone = pad.Zone + if(door.isOpen) { + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.DoorSlamsShut(door)) + } + } + + case HartTimer.UnlockDoors => + managedDoors.foreach { _.Actor ! Door.UpdateMechanism(OrbitalShuttlePadControl.shuttleIsBoarding) } + + case HartTimer.ShuttleDocked(forChannel) => + HartTimerActions.ShuttleDocked(pad, shuttle, forChannel) + + case HartTimer.ShuttleFreeFromDock(forChannel) => + HartTimerActions.ShuttleFreeFromDock(pad, shuttle, forChannel) + + case HartTimer.ShuttleStateUpdate(forChannel, state) => + HartTimerActions.ShuttleStateUpdate(pad, shuttle, forChannel, state) + + case _ => ; + } + + /** wire the pad and shuttle into a zone-scoped service handler */ + val shuttleTime: Receive = { + case Zone.Vehicle.HasSpawned(_, newShuttle: OrbitalShuttle) => + shuttle = newShuttle + pad.shuttle = newShuttle + pad.Owner.Amenities = new ShuttleAmenity(newShuttle) + ServiceManager.serviceManager ! ServiceManager.Lookup("hart") + + case ServiceManager.LookupResult(_, timer) => + timer ! HartTimer.PairWith(pad.Zone, pad.GUID, shuttle.GUID, self) + context.become(taxiing) + + case Zone.Vehicle.CanNotSpawn(zone, _, reason) => + org.log4s + .getLogger("OrbitalShuttle") + .error(s"shuttle for pad#${pad.Owner.GUID.guid} in zone ${zone.id} did not spawn - $reason") + //seal doors + managedDoors.foreach { _.Actor ! Door.UpdateMechanism(OrbitalShuttlePadControl.lockedWaitingForShuttle) } + + case msg: HartTimer.Command => + self.forward(msg) //delay? + + case _ => ; + } + + /** collect all of the doors that will be controlled by the HART system; + * set up the shuttle information based on the pad to which it belongs; + * register and add the shuttle as a common vehicle of the said zone + */ + val startUp: Receive = { + case Service.Startup() => + import net.psforever.types.Vector3 + import net.psforever.types.Vector3.DistanceSquared + import net.psforever.objects.GlobalDefinitions._ + val position = pad.Position + val zone = pad.Zone + //collect managed doors + managedDoors = pad.Owner.Amenities + .collect { case d: Door if d.Definition == gr_door_mb_orb => d } + .sortBy { o => DistanceSquared(position, o.Position) } + .take(8) + //create shuttle + val newShuttle = new OrbitalShuttle(orbital_shuttle) + newShuttle.Position = position + Vector3(0, -8.25f, 0).Rz(pad.Orientation.z) //magic offset number + newShuttle.Orientation = pad.Orientation + newShuttle.Faction = pad.Faction + zone.tasks ! OrbitalShuttlePadControl.registerShuttle(zone, newShuttle, self) + context.become(shuttleTime) + + case _ => ; + } +} + +object OrbitalShuttlePadControl { + /** + * Register the shuttle as a common vehicle in a zone. + * @param zone the zone the shuttle and the pad will occupy + * @param shuttle the vehicle that will be the shuttle + * @param ref a reference to the control agency for the orbital shuttle pad + * @return a `TaskResolver.GiveTask` object + */ + def registerShuttle(zone: Zone, shuttle: Vehicle, ref: ActorRef): TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localZone = zone + private val localShuttle = shuttle + private val localSelf = ref + + override def Description: String = s"register an orbital shuttle" + + override def isComplete : Task.Resolution.Value = if (localShuttle.HasGUID) { + Task.Resolution.Success + } else { + Task.Resolution.Incomplete + } + + def Execute(resolver : ActorRef) : Unit = { + localZone.Transport.tell(Zone.Vehicle.Spawn(localShuttle), localSelf) + resolver ! Success(true) + } + + override def onFailure(ex : Throwable) : Unit = { + super.onFailure(ex) + localSelf ! Zone.Vehicle.CanNotSpawn(localZone, localShuttle, ex.getMessage) + } + }, List(GUIDTask.RegisterVehicle(shuttle)(zone.GUID)) + ) + } + + /** + * Logic for door mechanism that allows the shuttle entryway to be opened. + * Only opens for users with proper faction affinity. + * @param obj what attempted to open the door + * @param door the door + * @return `true`, if the user is the accepted by the door; + * `false`, otherwise + */ + def shuttleIsBoarding(obj: PlanetSideServerObject, door: Door): Boolean = { + obj.Faction == door.Faction + } + + /** + * Logic for door mechanism that keeps select doors shut when the shuttle is not ready for boarding. + * A message flashes onscreen to explain this reason. + * The message will not flash if the door has no expectation of ever opening for a user. + * @see `AvatarAction.SendResponse` + * @see `AvatarServiceMessage` + * @see `ChatMessageType` + * @see `ChatMsg` + * @see `Player` + * @see `Service` + * @see `Zone.AvatarEvents` + * @param obj what attempted to open the door + * @param door the door + * @return `false`, as the door can not be opened in this state + */ + def lockedWaitingForShuttle(obj: PlanetSideServerObject, door: Door): Boolean = { + val zone = door.Zone + obj match { + case p: Player if p.Faction == door.Faction => + zone.AvatarEvents ! AvatarServiceMessage( + p.Name, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + ChatMsg(ChatMessageType.UNK_225, false, "", "@DoorWillOpenWhenShuttleReturns", None) + ) + ) + p.Name + case _ => ; + } + false + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala new file mode 100644 index 000000000..e7fdd8fbc --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala @@ -0,0 +1,42 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import akka.actor.ActorRef +import net.psforever.objects.serverobject.structures.{Amenity, AmenityDefinition} +import net.psforever.types.PlanetSideGUID + +/** + * A pseudo-`Amenity` of the high-altitude rapid transport (HART) building + * whose sole purpose is to allow the HART orbital shuttle to be initialized + * as if it were a normal `Amenity`-level feature of the building. + * This should not be considered an actual game object as defined by the game. + * It should resemble the orbital shuttle that it wraps in most important measurable ways. + * @see `OrbitalShuttleControl` + * @throws `AssertionError` if the vehicle is not a `OrbitalShuttle` + * @param shuttle the shuttle + */ +class ShuttleAmenity(shuttle: OrbitalShuttle) extends Amenity { + override def GUID = shuttle.GUID + + override def GUID_=(guid: PlanetSideGUID) = GUID + + override def DamageModel = shuttle.DamageModel + + override def Actor = shuttle.Actor + + override def Actor_=(control: ActorRef) = Actor + + override def Health = shuttle.Health + + override def Faction = shuttle.Faction + + def Definition = ShuttleAmenity.definition +} + +object ShuttleAmenity { + final val definition = new AmenityDefinition(net.psforever.packet.game.objectcreate.ObjectClass.orbital_shuttle) { + Name = "orbital_shuttle_fake" + Damageable = false + Repairable = false + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala index 985f122ec..231db45f6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala @@ -21,11 +21,12 @@ trait CaptureTerminalAwareBehavior { if (CaptureTerminalAwareObject.isInstanceOf[Mountable]) { CaptureTerminalAwareObject.asInstanceOf[Mountable].Seats.filter(x => x._2.isOccupied).foreach(x => { val (seat_num, seat) = x + val user = seat.occupant.get CaptureTerminalAwareObject.Zone.VehicleEvents ! VehicleServiceMessage( CaptureTerminalAwareObject.Zone.id, - VehicleAction.KickPassenger(seat.Occupant.get.GUID, seat_num, true, CaptureTerminalAwareObject.GUID)) - - seat.Occupant = None + VehicleAction.KickPassenger(user.GUID, seat_num, true, CaptureTerminalAwareObject.GUID) + ) + seat.unmount(user) }) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala index 9a37c1a34..7e0dee5ab 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala @@ -1,14 +1,9 @@ package net.psforever.objects.serverobject.terminals.capture -import net.psforever.actors.zone.BuildingActor import net.psforever.objects.Player import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.packet.game.PlanetsideAttributeEnum import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.types.PlanetSideEmpire -import java.util.concurrent.TimeUnit import scala.util.{Failure, Success} object CaptureTerminals { diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala index 063f1560e..1622650b5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala @@ -1,12 +1,10 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals.implant -import net.psforever.objects.Player import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.mount.{Mountable, Seat} import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware -import net.psforever.objects.vehicles.Seat import net.psforever.packet.game.TriggeredSound import net.psforever.types.Vector3 @@ -20,28 +18,12 @@ class ImplantTerminalMech(private val idef: ImplantTerminalMechDefinition) with Mountable with Hackable with CaptureTerminalAware { - private val seats: Map[Int, Seat] = Map(0 -> new Seat(idef.Seats(0))) + seats = Map(0 -> new Seat(idef.Seats.head._2)) HackSound = TriggeredSound.HackTerminal HackEffectDuration = Array(0, 30, 60, 90) HackDuration = Array(0, 10, 5, 3) - def Seats: Map[Int, Seat] = seats - - def Seat(seatNum: Int): Option[Seat] = seats.get(seatNum) - - def MountPoints: Map[Int, Int] = idef.MountPoints - - def GetSeatFromMountPoint(mount: Int): Option[Int] = idef.MountPoints.get(mount) - - def PassengerInSeat(user: Player): Option[Int] = { - if (seats(0).Occupant.contains(user)) { - Some(0) - } else { - None - } - } - def Definition: ImplantTerminalMechDefinition = idef } diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala index d2b386b37..4ab3462e9 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala @@ -21,8 +21,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} class ImplantTerminalMechControl(mech: ImplantTerminalMech) extends PoweredAmenityControl with FactionAffinityBehavior.Check - with MountableBehavior.Mount - with MountableBehavior.Dismount + with MountableBehavior with HackableBehavior.GenericHackable with DamageableEntity with RepairableEntity @@ -68,11 +67,11 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) case _ => ; } - override protected def MountTest( - obj: PlanetSideServerObject with Mountable, - seatNumber: Int, - player: Player - ): Boolean = { + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player + ): Boolean = { val zone = obj.Zone zone.map.terminalToInterface.get(obj.GUID.guid) match { case Some(interface_guid) => @@ -80,7 +79,7 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) case Some(interface) => !interface.Destroyed case None => false }) && - super.MountTest(obj, seatNumber, player) + super.mountTest(obj, seatNumber, player) case None => false } @@ -122,9 +121,9 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) val zoneId = zone.id val events = zone.VehicleEvents mech.Seats.values.foreach(seat => - seat.Occupant match { + seat.occupant match { case Some(player) => - seat.Occupant = None + seat.unmount(player) player.VehicleSeated = None if (player.HasGUID) { events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala index 0b351212d..7720b83df 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals.implant -import net.psforever.objects.definition.SeatDefinition +import net.psforever.objects.serverobject.mount.{MountInfo, MountableDefinition, SeatDefinition, Unrestricted} import net.psforever.objects.serverobject.structures.AmenityDefinition /** @@ -9,14 +9,15 @@ import net.psforever.objects.serverobject.structures.AmenityDefinition * Implant terminals are composed of two components. * This `Definition` constructs the visible mechanical tube component that can be mounted. */ -class ImplantTerminalMechDefinition extends AmenityDefinition(410) { - /* key - seat index, value - seat object */ - private val seats: Map[Int, SeatDefinition] = Map(0 -> new SeatDefinition) - /* key - entry point index, value - seat index */ - private val mountPoints: Map[Int, Int] = Map(1 -> 0) +class ImplantTerminalMechDefinition + extends AmenityDefinition(410) + with MountableDefinition { Name = "implant_terminal_mech" - def Seats: Map[Int, SeatDefinition] = seats - - def MountPoints: Map[Int, Int] = mountPoints + /* key - mount index, value - mount object */ + Seats += 0 -> new SeatDefinition() { + restriction = Unrestricted + } + /* key - entry point index, value - mount index */ + MountPoints += 1 -> MountInfo(0) } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala index cc4de5eb9..ae056a587 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala @@ -13,8 +13,6 @@ class FacilityTurret(tDef: FacilityTurretDefinition) with CaptureTerminalAware { WeaponTurret.LoadDefinition(this) - def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap - def Definition: FacilityTurretDefinition = tDef } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 53a27e492..d735fe689 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -3,8 +3,8 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool} import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons} -import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret} import net.psforever.objects.serverobject.hackable.GenericHackables @@ -31,8 +31,7 @@ import scala.concurrent.duration._ class FacilityTurretControl(turret: FacilityTurret) extends PoweredAmenityControl with FactionAffinityBehavior.Check - with MountableBehavior.TurretMount - with MountableBehavior.Dismount + with MountableBehavior with DamageableWeaponTurret with RepairableWeaponTurret with AmenityAutoRepair @@ -74,7 +73,7 @@ class FacilityTurretControl(turret: FacilityTurret) item.Magazine > 0 && turret.Seats.values.forall(!_.isOccupied) => TurretUpgrade.values.find(_.id == upgradeValue) match { case Some(upgrade) - if turret.Upgrade != upgrade && turret.Definition.Weapons.values + if turret.Upgrade != upgrade && turret.Definition.WeaponPaths.values .flatMap(_.keySet) .exists(_ == upgrade) => sender() ! CommonMessages.Progress( @@ -103,7 +102,7 @@ class FacilityTurretControl(turret: FacilityTurret) if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { weapon.Magazine += 1 val seat = turret.Seat(0).get - seat.Occupant match { + seat.occupant match { case Some(player: Player) => turret.Zone.LocalEvents ! LocalServiceMessage( turret.Zone.id, @@ -126,6 +125,13 @@ class FacilityTurretControl(turret: FacilityTurret) case _ => ; } + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player): Boolean = { + (!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed + } + override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = { tryAutoRepair() super.DamageAwareness(target, cause, amount) @@ -172,9 +178,9 @@ class FacilityTurretControl(turret: FacilityTurret) val zoneId = zone.id val events = zone.VehicleEvents turret.Seats.values.foreach(seat => - seat.Occupant match { + seat.occupant match { case Some(player) => - seat.Occupant = None + seat.unmount(player) player.VehicleSeated = None if (player.HasGUID) { events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala index 136c846cd..def85e459 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala @@ -9,7 +9,9 @@ import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance * The definition for any `FacilityTurret`. * @param objectId the object's identifier number */ -class FacilityTurretDefinition(private val objectId: Int) extends AmenityDefinition(objectId) with TurretDefinition { +class FacilityTurretDefinition(private val objectId: Int) + extends AmenityDefinition(objectId) + with TurretDefinition { DamageUsing = DamageCalculations.AgainstVehicle ResistUsing = StandardVehicleResistance Model = SimpleResolutions.calculate diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala index 9c6e0de98..9936abab3 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala @@ -2,7 +2,7 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition} -import net.psforever.objects.vehicles.Turrets +import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets} import net.psforever.objects.vital.resistance.ResistanceProfileMutators import net.psforever.objects.vital.resolution.DamageResistanceModel @@ -11,14 +11,14 @@ import scala.collection.mutable /** * The definition for any `MannedTurret`. */ -trait TurretDefinition extends ResistanceProfileMutators with DamageResistanceModel { +trait TurretDefinition + extends MountableWeaponsDefinition + with ResistanceProfileMutators + with DamageResistanceModel { odef: ObjectDefinition => Turrets(odef.ObjectId) //let throw NoSuchElementException - /* key - entry point index, value - seat index */ - private val mountPoints: mutable.HashMap[Int, Int] = mutable.HashMap() - /* key - seat number, value - hash map (below) */ /* key - upgrade, value - weapon definition */ - private val weapons: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = + private val weaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]]() /** can only be mounted by owning faction when `true` */ @@ -29,9 +29,7 @@ trait TurretDefinition extends ResistanceProfileMutators with DamageResistanceMo */ private var hasReserveAmmunition: Boolean = false - def MountPoints: mutable.HashMap[Int, Int] = mountPoints - - def Weapons: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weapons + def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths def FactionLocked: Boolean = factionLocked diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala index 74b8d8b9d..f9a036c46 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala @@ -1,22 +1,22 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.turret -import net.psforever.objects.{AmmoBox, PlanetSideGameObject, Player, Tool} -import net.psforever.objects.definition.{AmmoBoxDefinition, SeatDefinition, ToolDefinition} -import net.psforever.objects.equipment.{Equipment, EquipmentSlot} +import net.psforever.objects.{AmmoBox, PlanetSideGameObject, Tool} +import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition} +import net.psforever.objects.equipment.EquipmentSlot import net.psforever.objects.inventory.{Container, GridInventory} import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.vehicles.{MountedWeapons, Seat => Chair} +import net.psforever.objects.serverobject.mount.{SeatDefinition, Seat => Chair} +import net.psforever.objects.vehicles.MountableWeapons -trait WeaponTurret extends FactionAffinity with Mountable with MountedWeapons with Container { +trait WeaponTurret + extends FactionAffinity + with MountableWeapons + with Container { _: PlanetSideGameObject => - /** manned turrets have just one seat; this is just standard interface */ - protected val seats: Map[Int, Chair] = Map(0 -> Chair(new SeatDefinition() { ControlledWeapon = Some(1) })) - - /** turrets have just one weapon; this is just standard interface */ - protected var weapons: Map[Int, EquipmentSlot] = Map.empty + /** manned turrets have just one mount; this is just standard interface */ + seats = Map(0 -> new Chair(new SeatDefinition())) /** may or may not have inaccessible inventory space * see `ReserveAmmunition` in the definition @@ -45,39 +45,6 @@ trait WeaponTurret extends FactionAffinity with Mountable with MountedWeapons wi def VisibleSlots: Set[Int] = Set(1) - def Weapons: Map[Int, EquipmentSlot] = weapons - - def MountPoints: Map[Int, Int] - - def Seats: Map[Int, Chair] = seats - - def Seat(seatNum: Int): Option[Chair] = seats.get(seatNum) - - /** - * Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it. - * @param mountPoint an index representing the seat position / mounting point - * @return a seat number, or `None` - */ - def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { - MountPoints.get(mountPoint) - } - - def PassengerInSeat(user: Player): Option[Int] = { - if (seats(0).Occupant.contains(user)) { - Some(0) - } else { - None - } - } - - def ControlledWeapon(wepNumber: Int): Option[Equipment] = { - if (VisibleSlots.contains(wepNumber)) { - weapons(wepNumber).Equipment - } else { - None - } - } - def Upgrade: TurretUpgrade.Value = upgradePath def Upgrade_=(upgrade: TurretUpgrade.Value): TurretUpgrade.Value = { @@ -86,7 +53,7 @@ trait WeaponTurret extends FactionAffinity with Mountable with MountedWeapons wi //upgrade each weapon as long as that weapon has a valid option for that upgrade Definition match { case definition: TurretDefinition => - definition.Weapons.foreach({ + definition.WeaponPaths.foreach({ case (index, upgradePaths) => if (upgradePaths.contains(upgrade)) { updated = true @@ -136,7 +103,7 @@ object WeaponTurret { def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = { import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon //create weapons; note the class - turret.weapons = tdef.Weapons + turret.weapons = tdef.WeaponPaths .map({ case (num, upgradePaths) => val slot = EquipmentSlot(BaseTurretWeapon) @@ -146,7 +113,7 @@ object WeaponTurret { .toMap //special inventory ammunition object(s) if (tdef.ReserveAmmunition) { - val allAmmunitionTypes = tdef.Weapons.values.flatMap { _.values.flatMap { _.AmmoTypes } }.toSet + val allAmmunitionTypes = tdef.WeaponPaths.values.flatMap { _.values.flatMap { _.AmmoTypes } }.toSet if (allAmmunitionTypes.nonEmpty) { turret.inventory.Resize(allAmmunitionTypes.size, 1) var i: Int = 0 diff --git a/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala b/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala index 8e608ffe9..860959c6f 100644 --- a/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala +++ b/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala @@ -3,9 +3,9 @@ package net.psforever.objects.vehicles /** * An `Enumeration` of various permission groups that control access to aspects of a vehicle.
- * - `Driver` is a seat that is always seat number 0.
- * - `Gunner` is a seat that is not the `Driver` and controls a mounted weapon.
- * - `Passenger` is a seat that is not the `Driver` and does not have control of a mounted weapon.
+ * - `Driver` is a mount that is always mount number 0.
+ * - `Gunner` is a mount that is not the `Driver` and controls a mounted weapon.
+ * - `Passenger` is a mount that is not the `Driver` and does not have control of a mounted weapon.
* - `Trunk` represnts access to the vehicle's internal storage space.
* Organized to replicate the `PlanetsideAttributeMessage` value used for that given access level. * In their respective `PlanetsideAttributeMessage` packet, the groups are indexed in the same order as 10 through 13. diff --git a/src/main/scala/net/psforever/objects/vehicles/Cargo.scala b/src/main/scala/net/psforever/objects/vehicles/Cargo.scala index c2e1a71f8..736b771e4 100644 --- a/src/main/scala/net/psforever/objects/vehicles/Cargo.scala +++ b/src/main/scala/net/psforever/objects/vehicles/Cargo.scala @@ -1,88 +1,11 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2021 PSForever package net.psforever.objects.vehicles import net.psforever.objects.Vehicle -import net.psforever.objects.definition.{CargoDefinition} +import net.psforever.objects.serverobject.mount.{MountableSpace, MountableSpaceDefinition} -/** - * Server-side support for a slot that vehicles can occupy - * @param cargoDef the Definition that constructs this item and maintains some of its unchanging fields - */ -class Cargo(private val cargoDef: CargoDefinition) { - private var occupant: Option[Vehicle] = None +class Cargo(private val cdef: MountableSpaceDefinition[Vehicle]) extends MountableSpace[Vehicle] { + override protected def testToMount(target: Vehicle): Boolean = target.MountedIn.isEmpty && super.testToMount(target) - /** - * Is the cargo hold occupied? - * @return The vehicle in the cargo hold, or `None` if it is left vacant - */ - def Occupant: Option[Vehicle] = { - this.occupant - } - - /** - * A vehicle is trying to board the cargo hold - * Cargo holds are exclusive positions that can only hold one vehicle at a time. - * @param vehicle the vehicle boarding the cargo hold, or `None` if the vehicle is leaving - * @return the vehicle sitting in this seat, or `None` if it is left vacant - */ - def Occupant_=(vehicle: Vehicle): Option[Vehicle] = Occupant_=(Some(vehicle)) - - def Occupant_=(vehicle: Option[Vehicle]): Option[Vehicle] = { - if (vehicle.isDefined) { - if (this.occupant.isEmpty) { - this.occupant = vehicle - } - } else { - this.occupant = None - } - this.occupant - } - - /** - * Is this cargo hold occupied? - * @return `true`, if it is occupied; `false`, otherwise - */ - def isOccupied: Boolean = { - this.occupant.isDefined - } - - def CargoRestriction: CargoVehicleRestriction.Value = { - cargoDef.CargoRestriction - } - - def Bailable: Boolean = { - cargoDef.Bailable - } - - /** - * Override the string representation to provide additional information. - * @return the string output - */ - override def toString: String = { - Cargo.toString(this) - } -} - -object Cargo { - - /** - * Overloaded constructor. - * @return a `Cargo` object - */ - def apply(cargoDef: CargoDefinition): Cargo = { - new Cargo(cargoDef) - } - - /** - * Provide a fixed string representation. - * @return the string output - */ - def toString(obj: Cargo): String = { - val cargoStr = if (obj.isOccupied) { - s", occupied by vehicle ${obj.Occupant.get.GUID}" - } else { - "" - } - s"cargo$cargoStr" - } + def definition: MountableSpaceDefinition[Vehicle] = cdef } diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala index 1262b3c31..509525cf4 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala @@ -149,7 +149,7 @@ object CargoBehavior { //cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it log.info(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") cargo.MountedIn = carrierGUID - hold.Occupant = cargo + hold.mount(cargo) cargo.Velocity = None zone.VehicleEvents ! VehicleServiceMessage( s"${cargo.Actor}", @@ -168,7 +168,7 @@ object CargoBehavior { log.info( "HandleCheckCargoMounting: cargo vehicle is too far away or didn't mount within allocated time - aborting" ) - val cargoDriverGUID = cargo.Seats(0).Occupant.get.GUID + val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.SendResponse( @@ -266,7 +266,7 @@ object CargoBehavior { log.info( s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance" ) - val cargoDriverGUID = cargo.Seats(0).Occupant.get.GUID + val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.SendResponse( @@ -289,7 +289,7 @@ object CargoBehavior { } else if (iteration > 40) { //cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold cargo.MountedIn = carrierGUID - hold.Occupant = cargo + hold.mount(cargo) CargoMountBehaviorForAll(carrier, cargo, mountPoint) false } else { @@ -363,11 +363,11 @@ object CargoBehavior { kicked: Boolean ): Unit = { val zone = carrier.Zone - carrier.CargoHolds.find({ case (_, hold) => hold.Occupant.contains(cargo) }) match { + carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match { case Some((mountPoint, hold)) => cargo.MountedIn = None - hold.Occupant = None - val driverOpt = cargo.Seats(0).Occupant + hold.unmount(cargo) + val driverOpt = cargo.Seats(0).occupant val rotation: Vector3 = if (Vehicles.CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set //dismount router "sideways" in a lodestar carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360) @@ -393,7 +393,7 @@ object CargoBehavior { s"$cargoActor", VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) ) - if (carrier.Flying) { + if (carrier.isFlying) { //the carrier vehicle is flying; eject the cargo vehicle val ejectCargoMsg = CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.InProgress, 0) diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala b/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala index 19b2b1503..483fc06ab 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala @@ -2,11 +2,11 @@ package net.psforever.objects.vehicles /** - * An `Enumeration` of exo-suit-based seat access restrictions.
+ * An `Enumeration` of exo-suit-based mount access restrictions.
*
- * The default value is `NoMax` as that is the most common seat. + * The default value is `NoMax` as that is the most common mount. * `NoReinforcedOrMax` is next most common. - * `MaxOnly` is a rare seat restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. + * `MaxOnly` is a rare mount restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. */ object CargoVehicleRestriction extends Enumeration { type Type = Value diff --git a/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala b/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala new file mode 100644 index 000000000..fdbba2b20 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.serverobject.mount.Mountable + +trait MountableWeapons + extends MountedWeapons + with Mountable { + this: PlanetSideGameObject => + + /** + * Given a valid mount number, retrieve an index where the weapon controlled from this mount is mounted. + * @param seatNumber the mount number + * @return a mounted weapon by index, or `None` if either the mount doesn't exist or there is no controlled weapon + */ + def WeaponControlledFromSeat(seatNumber: Int): Option[Equipment] = { + Definition + .asInstanceOf[MountableWeaponsDefinition] + .controlledWeapons.get(seatNumber) match { + case Some(wepNumber) if seats.get(seatNumber).nonEmpty => controlledWeapon(wepNumber) + case _ => None + } + } + + def controlledWeapon(wepNumber: Int): Option[Equipment] = ControlledWeapon(wepNumber) + + def ControlledWeapon(wepNumber: Int): Option[Equipment] = { + weapons.get(wepNumber) match { + case Some(slot) => slot.Equipment + case _ => None + } + } + + def Definition: MountableWeaponsDefinition +} + diff --git a/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala b/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala new file mode 100644 index 000000000..f897b913c --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.serverobject.mount.MountableDefinition + +import scala.collection.mutable + +trait MountableWeaponsDefinition + extends MountedWeaponsDefinition + with MountableDefinition { + val controlledWeapons: mutable.HashMap[Int, Int] = mutable.HashMap[Int, Int]() +} diff --git a/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala b/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala index 35f675464..997e9f153 100644 --- a/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala +++ b/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala @@ -2,38 +2,13 @@ package net.psforever.objects.vehicles import net.psforever.objects.PlanetSideGameObject -import net.psforever.objects.equipment.{Equipment, EquipmentSlot} -import net.psforever.objects.inventory.Container -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.vehicles.{Seat => Chair} +import net.psforever.objects.equipment.EquipmentSlot trait MountedWeapons { - this: PlanetSideGameObject with Mountable with Container => + this: PlanetSideGameObject => + protected var weapons: Map[Int, EquipmentSlot] = Map[Int, EquipmentSlot]() - def Weapons: Map[Int, EquipmentSlot] + def Weapons: Map[Int, EquipmentSlot] = weapons - /** - * Given a valid seat number, retrieve an index where the weapon controlled from this seat is mounted. - * @param seatNumber the seat number - * @return a mounted weapon by index, or `None` if either the seat doesn't exist or there is no controlled weapon - */ - def WeaponControlledFromSeat(seatNumber: Int): Option[Equipment] = { - Seat(seatNumber) match { - case Some(seat) => - wepFromSeat(seat) - case None => - None - } - } - - private def wepFromSeat(seat: Chair): Option[Equipment] = { - seat.ControlledWeapon match { - case Some(index) => - ControlledWeapon(index) - case None => - None - } - } - - def ControlledWeapon(wepNumber: Int): Option[Equipment] + def Definition: MountedWeaponsDefinition } diff --git a/src/main/scala/net/psforever/objects/vehicles/MountedWeaponsDefinition.scala b/src/main/scala/net/psforever/objects/vehicles/MountedWeaponsDefinition.scala new file mode 100644 index 000000000..2ff759c74 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/MountedWeaponsDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.definition.ToolDefinition + +import scala.collection.mutable + +trait MountedWeaponsDefinition { + /* key - mount index (where this weapon attaches during object construction), value - the weapon on an EquipmentSlot */ + protected var weapons: mutable.HashMap[Int, ToolDefinition] = mutable.HashMap[Int, ToolDefinition]() + + def Weapons: mutable.HashMap[Int, ToolDefinition] = weapons +} diff --git a/src/main/scala/net/psforever/objects/vehicles/Seat.scala b/src/main/scala/net/psforever/objects/vehicles/Seat.scala deleted file mode 100644 index c32c3b315..000000000 --- a/src/main/scala/net/psforever/objects/vehicles/Seat.scala +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.vehicles - -import net.psforever.objects.definition.SeatDefinition -import net.psforever.objects.Player - -/** - * Server-side support for a slot that infantry players can occupy, ostensibly called a "seat" and treated like a "seat." - * (Players can sit in it.) - * @param seatDef the Definition that constructs this item and maintains some of its unchanging fields - */ -class Seat(private val seatDef: SeatDefinition) { - private var occupant: Option[Player] = None -// private var lockState : VehicleLockState.Value = VehicleLockState.Empire - - /** - * Is this seat occupied? - * @return the Player object of the player sitting in this seat, or `None` if it is left vacant - */ - def Occupant: Option[Player] = { - this.occupant - } - - /** - * The player is trying to sit down. - * Seats are exclusive positions that can only hold one occupant at a time. - * @param player the player who wants to sit, or `None` if the occupant is getting up - * @return the Player object of the player sitting in this seat, or `None` if it is left vacant - */ - def Occupant_=(player: Player): Option[Player] = Occupant_=(Some(player)) - - def Occupant_=(player: Option[Player]): Option[Player] = { - if (player.isDefined) { - if (this.occupant.isEmpty) { - this.occupant = player - } - } else { - this.occupant = None - } - this.occupant - } - - /** - * Is this seat occupied? - * @return `true`, if it is occupied; `false`, otherwise - */ - def isOccupied: Boolean = { - this.occupant.isDefined - } - -// def SeatLockState : VehicleLockState.Value = { -// this.lockState -// } -// -// def SeatLockState_=(lockState : VehicleLockState.Value) : VehicleLockState.Value = { -// this.lockState = lockState -// SeatLockState -// } - - def ArmorRestriction: SeatArmorRestriction.Value = { - seatDef.ArmorRestriction - } - - /** Determines if the seat can be bailed from while the vehicle is in motion */ - def Bailable: Boolean = { - seatDef.Bailable - } - - def ControlledWeapon: Option[Int] = { - seatDef.ControlledWeapon - } - - /** - * Override the string representation to provide additional information. - * @return the string output - */ - override def toString: String = { - Seat.toString(this) - } -} - -object Seat { - - /** - * Overloaded constructor. - * @return a `Seat` object - */ - def apply(seatDef: SeatDefinition): Seat = { - new Seat(seatDef) - } - - /** - * Provide a fixed string representation. - * @return the string output - */ - def toString(obj: Seat): String = { - val seatStr = if (obj.isOccupied) { - s", occupied by player ${obj.Occupant.get.GUID}" - } else { - "" - } - s"seat$seatStr" - } -} diff --git a/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala b/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala index b811d76b7..0da4c97cc 100644 --- a/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala +++ b/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala @@ -2,14 +2,15 @@ package net.psforever.objects.vehicles /** - * An `Enumeration` of exo-suit-based seat access restrictions.
+ * An `Enumeration` of exo-suit-based mount access restrictions.
*
- * The default value is `NoMax` as that is the most common seat type. + * The default value is `NoMax` as that is the most common mount type. * `NoReinforcedOrMax` is next most common. - * `MaxOnly` is a rare seat restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. + * `MaxOnly` is a rare mount restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. + * `Unrestricted` is for "seats" that do not limit by exo-suit type, such the orbital shuttle. */ object SeatArmorRestriction extends Enumeration { type Type = Value - val MaxOnly, NoMax, NoReinforcedOrMax = Value + val MaxOnly, NoMax, NoReinforcedOrMax, Unrestricted = Value } diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 80ce112a1..93492d76d 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -5,6 +5,7 @@ import akka.actor.{Actor, Cancellable} import net.psforever.objects._ import net.psforever.objects.ballistics.VehicleSource import net.psforever.objects.ce.TelepadLike +import net.psforever.objects.entity.WorldEntity import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons} import net.psforever.objects.guid.GUIDTask import net.psforever.objects.inventory.{GridInventory, InventoryItem} @@ -48,8 +49,7 @@ class VehicleControl(vehicle: Vehicle) extends Actor with FactionAffinityBehavior.Check with DeploymentBehavior - with MountableBehavior.Mount - with MountableBehavior.Dismount + with MountableBehavior with CargoBehavior with DamageableVehicle with RepairableVehicle @@ -129,34 +129,13 @@ class VehicleControl(vehicle: Vehicle) case Vehicle.Ownership(Some(player)) => GainOwnership(player) - case msg@Mountable.TryMount(player, seat_num) => - tryMountBehavior.apply(msg) - val obj = MountableObject - //check that the player has actually been sat in the expected seat - if (obj.PassengerInSeat(player).contains(seat_num)) { - //if the driver seat, change ownership - if (seat_num == 0 && !obj.OwnerName.contains(player.Name)) { - //whatever vehicle was previously owned - vehicle.Zone.GUID(player.avatar.vehicle) match { - case Some(v : Vehicle) => - v.Actor ! Vehicle.Ownership(None) - case _ => - player.avatar.vehicle = None - } - LoseOwnership() //lose our current ownership - GainOwnership(player) //gain new ownership - } - else { - decaying = false - decayTimer.cancel() - } - // - updateZoneInteractionProgressUI(player) - } + case msg @ Mountable.TryMount(player, mount_point) => + mountBehavior.apply(msg) + mountCleanup(mount_point, player) - case msg : Mountable.TryDismount => + case msg @ Mountable.TryDismount(_, seat_num) => dismountBehavior.apply(msg) - dismountCleanup() + dismountCleanup(seat_num) case Vehicle.ChargeShields(amount) => val now : Long = System.currentTimeMillis() @@ -261,7 +240,7 @@ class VehicleControl(vehicle: Vehicle) case Vehicle.Deconstruct(time) => time match { - case Some(delay) => + case Some(delay) if vehicle.Definition.undergoesDecay => decaying = true decayTimer.cancel() decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) @@ -288,13 +267,13 @@ class VehicleControl(vehicle: Vehicle) case msg : Deployment.TryUndeploy => deployBehavior.apply(msg) - case msg : Mountable.TryDismount => + case msg @ Mountable.TryDismount(_, seat_num) => dismountBehavior.apply(msg) - dismountCleanup() + dismountCleanup(seat_num) case Vehicle.Deconstruct(time) => time match { - case Some(delay) => + case Some(delay) if vehicle.Definition.undergoesDecay => decaying = true decayTimer.cancel() decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) @@ -327,46 +306,77 @@ class VehicleControl(vehicle: Vehicle) case _ => } - val tryMountBehavior : Receive = { - case msg @ Mountable.TryMount(user, seat_num) => - val exosuit = user.ExoSuit - val restriction = vehicle.Seats(seat_num).ArmorRestriction - val seatGroup = vehicle.SeatPermissionGroup(seat_num).getOrElse(AccessPermissionGroup.Passenger) - val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) - if ( - (if (seatGroup == AccessPermissionGroup.Driver) { - vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked - } - else { - permission != VehicleLockState.Locked - }) && - (exosuit match { - case ExoSuitType.MAX => restriction == SeatArmorRestriction.MaxOnly - case ExoSuitType.Reinforced => restriction == SeatArmorRestriction.NoMax - case _ => restriction != SeatArmorRestriction.MaxOnly - }) - ) { - mountBehavior.apply(msg) - } - else { - sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, seat_num)) - } + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + user: Player + ): Boolean = { + val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger) + val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) + (if (seatGroup == AccessPermissionGroup.Driver) { + vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked + } else { + permission != VehicleLockState.Locked + }) && + super.mountTest(obj, seatNumber, user) } - def dismountCleanup(): Unit = { + def mountCleanup(mount_point: Int, user: Player): Unit = { + val obj = MountableObject + obj.PassengerInSeat(user) match { + case Some(seatNumber) => + //if the driver mount, change ownership if that is permissible for this vehicle + if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) { + //whatever vehicle was previously owned + vehicle.Zone.GUID(user.avatar.vehicle) match { + case Some(v : Vehicle) => + v.Actor ! Vehicle.Ownership(None) + case _ => + user.avatar.vehicle = None + } + GainOwnership(user) //gain new ownership + } + else { + decaying = false + decayTimer.cancel() + } + updateZoneInteractionProgressUI(user) + case None => ; + } + } + + override protected def dismountTest( + obj: Mountable with WorldEntity, + seatNumber: Int, + user: Player + ): Boolean = { + vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user) + } + + def dismountCleanup(seatBeingDismounted: Int): Unit = { val obj = MountableObject // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount if (!obj.Seats(0).isOccupied) { obj.Velocity = Some(Vector3.Zero) } - //are we already decaying? are we unowned? is no one seated anywhere? - if (!decaying && obj.Owner.isEmpty && obj.Seats.values.forall(!_.isOccupied)) { - decaying = true - decayTimer = context.system.scheduler.scheduleOnce( - MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), - self, - VehicleControl.PrepareForDeletion() - ) + if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated + //we were only owning the vehicle while we sat in its driver seat + val canBeOwned = obj.Definition.CanBeOwned + if (canBeOwned.contains(false) && seatBeingDismounted == 0) { + LoseOwnership() + } + //are we already decaying? are we unowned? is no one seated anywhere? + if (!decaying && + obj.Definition.undergoesDecay && + obj.Owner.isEmpty && + obj.Seats.values.forall(!_.isOccupied)) { + decaying = true + decayTimer = context.system.scheduler.scheduleOnce( + MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), + self, + VehicleControl.PrepareForDeletion() + ) + } } } @@ -390,12 +400,12 @@ class VehicleControl(vehicle: Vehicle) ) case _ => ; } - if (!vehicle.Flying || kickPassengers) { + if (!vehicle.isFlying || kickPassengers) { //kick all passengers (either not flying, or being explicitly instructed) vehicle.Seats.values.foreach { seat => - seat.Occupant match { + seat.occupant match { case Some(player) => - seat.Occupant = None + seat.unmount(player) player.VehicleSeated = None if (player.HasGUID) { events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) @@ -408,7 +418,7 @@ class VehicleControl(vehicle: Vehicle) vehicle.CargoHolds.values .collect { case hold if hold.isOccupied => - val cargo = hold.Occupant.get + val cargo = hold.occupant.get CargoBehavior.HandleVehicleCargoDismount( cargo.GUID, cargo, @@ -423,10 +433,7 @@ class VehicleControl(vehicle: Vehicle) def PrepareForDeletion() : Unit = { decaying = false - val guid = vehicle.GUID val zone = vehicle.Zone - val zoneId = zone.id - val events = zone.VehicleEvents //miscellaneous changes Vehicles.BeforeUnloadVehicle(vehicle, zone) //cancel jammed behavior @@ -449,7 +456,10 @@ class VehicleControl(vehicle: Vehicle) def LoseOwnership(): Unit = { val obj = MountableObject Vehicles.Disown(obj.GUID, obj) - if (!decaying && obj.Seats.values.forall(!_.isOccupied)) { + if (!decaying && + obj.Definition.undergoesDecay && + obj.Owner.isEmpty && + obj.Seats.values.forall(!_.isOccupied)) { decaying = true decayTimer = context.system.scheduler.scheduleOnce( obj.Definition.DeconstructionTime.getOrElse(5 minutes), @@ -460,7 +470,9 @@ class VehicleControl(vehicle: Vehicle) } def GainOwnership(player: Player): Unit = { - Vehicles.Own(MountableObject, player) match { + val obj = MountableObject + Vehicles.Disown(obj.GUID, obj) + Vehicles.Own(obj, player) match { case Some(_) => decaying = false decayTimer.cancel() @@ -538,7 +550,7 @@ class VehicleControl(vehicle: Vehicle) val toChannel = if (obj.VisibleSlots.contains(fromSlot)) zone.id else self.toString zone.VehicleEvents ! VehicleServiceMessage( toChannel, - VehicleAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID) + VehicleAction.ObjectDelete(item.GUID) ) } @@ -558,7 +570,7 @@ class VehicleControl(vehicle: Vehicle) val zone = vehicle.Zone val zoneChannel = zone.id val GUID0 = Service.defaultPlayerGUID - val driverChannel = vehicle.Seats(0).Occupant match { + val driverChannel = vehicle.Seats(0).occupant match { case Some(tplayer) => tplayer.Name case None => "" } @@ -623,7 +635,7 @@ class VehicleControl(vehicle: Vehicle) val guid = vehicle.GUID val zone = vehicle.Zone val GUID0 = Service.defaultPlayerGUID - val driverChannel = vehicle.Seats(0).Occupant match { + val driverChannel = vehicle.Seats(0).occupant match { case Some(tplayer) => tplayer.Name case None => "" } @@ -686,7 +698,7 @@ class VehicleControl(vehicle: Vehicle) percentage, body, vehicle.Seats.values - .collect { case seat if seat.isOccupied => seat.Occupant.get } + .flatMap { case seat if seat.isOccupied => seat.occupants } .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) } ) } @@ -779,7 +791,7 @@ class VehicleControl(vehicle: Vehicle) percentage, body, vehicle.Seats.values - .collect { case seat if seat.isOccupied => seat.Occupant.get } + .flatMap { case seat if seat.isOccupied => seat.occupants } .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) } ) } diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala index fcf1f544d..2101ff342 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala @@ -2,6 +2,7 @@ package net.psforever.objects.vehicles import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.mount.Seat import net.psforever.objects.zones.Zone /** @@ -14,7 +15,7 @@ import net.psforever.objects.zones.Zone * @param vehicle the vehicle in transport * @param origin where the vehicle originally was * @param driverName the name of the driver when the transport process started - * @param passengers the paired names and seat indices of all passengers when the transport process started + * @param passengers the paired names and mount indices of all passengers when the transport process started * @param cargo the paired driver names and cargo hold indices of all cargo vehicles when the transport process started */ final case class VehicleManifest( @@ -28,17 +29,17 @@ final case class VehicleManifest( object VehicleManifest { def apply(vehicle: Vehicle): VehicleManifest = { - val driverName = vehicle.Seats(0).Occupant match { + val driverName = vehicle.Seats(0).occupant match { case Some(driver) => driver.Name case None => "MISSING_DRIVER" } val passengers = vehicle.Seats.collect { - case (index, seat) if index > 0 && seat.isOccupied => - (seat.Occupant.get.Name, index) + case (index: Int, seat: Seat) if index > 0 && seat.isOccupied => + (seat.occupant.get.Name, index) } val cargo = vehicle.CargoHolds.collect { - case (index, hold) if hold.Occupant.nonEmpty => - hold.Occupant.get.Seats(0).Occupant match { + case (index: Int, hold: Cargo) if hold.occupant.nonEmpty => + hold.occupant.get.Seats(0).occupant match { case Some(driver) => (driver.Name, index) case None => 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 da2178fb1..7157375ad 100644 --- a/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala +++ b/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala @@ -392,7 +392,7 @@ object ProjectileDamageModifierFunctions { data: DamageInteraction, cause: ProjectileReason ): Int = { - if (cause.resolution == resolution) { + if (data.resolution == resolution) { (data.cause.source.Aggravated, data.target) match { case (Some(aggravation), p: PlayerSource) => val degradation = aggravation.info.find(_.damage_type == damageType) match { diff --git a/src/main/scala/net/psforever/objects/zones/MapInfo.scala b/src/main/scala/net/psforever/objects/zones/MapInfo.scala index 8191c47b8..6151cce22 100644 --- a/src/main/scala/net/psforever/objects/zones/MapInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/MapInfo.scala @@ -2,7 +2,7 @@ package net.psforever.objects.zones import enumeratum.values.{StringEnum, StringEnumEntry} import net.psforever.objects.serverobject.environment._ -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideGUID, Vector3} sealed abstract class MapInfo( val value: String, @@ -180,7 +180,7 @@ case object MapInfo extends StringEnum[MapInfo] { Pool(EnvironmentAttribute.Water, 34.96875f, 5899.367f, 3235.5781f, 5573.8516f, 2865.7812f), //northeast of hart c campus Pool(EnvironmentAttribute.Water, 34.328125f, 3880.7422f, 5261.508f, 3780.9219f, 5166.953f), //east of hart a campus Pool(EnvironmentAttribute.Water, 31.03125f, 4849.797f, 2415.4297f, 4731.8594f, 2252.1484f) //south of hart c campus - ) + ) ++ MapEnvironment.map11Environment ) case object Map12 @@ -188,7 +188,8 @@ case object MapInfo extends StringEnum[MapInfo] { value = "map12", checksum = 962888126L, scale = MapScale.Dim8192, - environment = List(SeaLevel(EnvironmentAttribute.Water, 20.03125f)) + environment = List(SeaLevel(EnvironmentAttribute.Water, 20.03125f)) ++ + MapEnvironment.map12Environment ) case object Map13 @@ -196,7 +197,8 @@ case object MapInfo extends StringEnum[MapInfo] { value = "map13", checksum = 3904659548L, scale = MapScale.Dim8192, - environment = List(SeaLevel(EnvironmentAttribute.Water, 30)) + environment = List(SeaLevel(EnvironmentAttribute.Water, 30)) ++ + MapEnvironment.map13Environment ) case object Map14 @@ -317,3 +319,107 @@ case object MapInfo extends StringEnum[MapInfo] { val values: IndexedSeq[MapInfo] = findValues } + +object MapEnvironment { + /** the pattern of mount points for the HART gantries in most facilities; + * eight values - 1-8 - listed as four downstairs - NE SE NW SW - then four upstairs - same + */ + private val hartMountPoints: Seq[Int] = Seq(6,5, 2,1, 8,7, 4,3) + /** the pattern of mount points for the HART gantries in VS sanctuary facilities; + * eight values - 1-8 - listed as four downstairs - NE SE NW SW - then four upstairs - same + */ + private val vsHartMountPoints: Seq[Int] = Seq(1,2, 5,6, 3,4, 7,8) + + /** HART denial fields for the New Conglomerate sanctuary */ + final val map11Environment: List[PieceOfEnvironment] = + hartGantryDenialFields(PlanetSideGUID(840), Vector3(2258, 5538, 65.20142f), hartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(841), Vector3(4152, 6070, 43.8766136f), hartMountPoints) ++ + specialHartGantryDenialFields(PlanetSideGUID(842)) + + /** HART denial fields for the Terran Republic sanctuary */ + final val map12Environment: List[PieceOfEnvironment] = + hartGantryDenialFields(PlanetSideGUID(808), Vector3(2922, 5230, 35.9989929f), hartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(809), Vector3(3006, 2984, 34.919342f), hartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(810), Vector3(5232, 3908, 35.9291039f), hartMountPoints) + + /** HART denial fields for the Vanu Sovereignty sanctuary */ + final val map13Environment: List[PieceOfEnvironment] = + hartGantryDenialFields(PlanetSideGUID(786), Vector3(2978, 4834, 56.085392f), vsHartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(787), Vector3(3688, 2808, 90.85312f), vsHartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(788), Vector3(5610, 4238, 103.228859f), vsHartMountPoints) + + /** + * Generate eight environmental representations that serve to eject players + * from the high altitude rapid transport (HART) building boarding gantry hallways + * when the HART shuttle associated with that building is no longer boarding + * and the doors to those hallways should deny entrance. + * When kicked out of the hallway, + * ejected players should be placed in the same position as if the player willingly dismounted the shuttle.
+ *
+ * While this task seems daunting, HART buildings are formulaic, not only in layout but in orientation. + * @param obbasemesh the globally unique identifier of the orbital shuttle pad, + * an amenity of an `orbital_building_*` + * @param position a very specific position near the center of the `orbital_building_*` building + * @param mountPoints the assignment of mount point for each denial field + * @return a list of environmental representations + */ + private def hartGantryDenialFields( + obbasemesh: PlanetSideGUID, + position: Vector3, + mountPoints: Seq[Int] + ): List[PieceOfEnvironment] = { + val px: Float = position.x + val py: Float = position.y + val pz: Float = position.z + val wall: Float = 14.7188f + val door: Float = 55.9219f + val gantry: Float = 45.9297f + val lower: Float = pz + 6.164608f + val upper: Float = pz + 17.508358f + //downstairs lobbies are listed before upstairs lobbies to ensure they are tested first + List( + GantryDenialField(obbasemesh, mountPoints(0), DeepSurface(lower, py + wall, px + door, py + 1, px + gantry)), //NE + GantryDenialField(obbasemesh, mountPoints(1), DeepSurface(lower, py - 1, px + door, py - wall, px + gantry)), //SE + GantryDenialField(obbasemesh, mountPoints(2), DeepSurface(lower, py + wall, px - gantry, py + 1, px - door)), //NW + GantryDenialField(obbasemesh, mountPoints(3), DeepSurface(lower, py - 1, px - gantry, py - wall, px - door)), //SW + GantryDenialField(obbasemesh, mountPoints(4), DeepSurface(upper, py + wall, px + door, py + 1, px + gantry)), //NE + GantryDenialField(obbasemesh, mountPoints(5), DeepSurface(upper, py - 1, px + door, py - wall, px + gantry)), //SE + GantryDenialField(obbasemesh, mountPoints(6), DeepSurface(upper, py + wall, px - gantry, py + 1, px - door)), //NW + GantryDenialField(obbasemesh, mountPoints(7), DeepSurface(upper, py - 1, px - gantry, py - wall, px - door)) //SW + ) + } + + /** + * Generate eight environmental representations that serve to eject players + * from the high altitude rapid transport (HART) building boarding hallways + * when the HART shuttle associated with that building is no longer boarding + * and the doors to those hallways should deny entrance. + * When kicked out of the hallway, + * ejected players should be placed in the same position as if the player willingly dismounted the shuttle.
+ *
+ * The New Conglomerate HART A campus building is at an ordinal angle + * which makes the typical axis-aligned environment geometry unsuitable for representation of the denial field. + * Instead of rectangles, circles will be used. + * This facility is centered at 4816, 3506, 68.73806 (x ,y, z). + * @param obbasemesh the globally unique identifier of the orbital shuttle pad, + * an amenity of an `orbital_building_*` + * @return a list of environmental representations + */ + def specialHartGantryDenialFields(obbasemesh: PlanetSideGUID): List[PieceOfEnvironment] = { + val lower: Float = 74.902668f + val upper: Float = 86.246418f + val radius: Float = 6.5f + //downstairs lobbies are listed before upstairs lobbies to ensure they are tested first + List( + GantryDenialField(obbasemesh, 1, DeepCircularSurface(Vector3(4846f, 3547.6016f, lower), radius)), //N + GantryDenialField(obbasemesh, 2, DeepCircularSurface(Vector3(4857.5234f, 3536f, lower), radius)), //E + GantryDenialField(obbasemesh, 5, DeepCircularSurface(Vector3(4774.3516f, 3476f, lower), radius)), //W + GantryDenialField(obbasemesh, 6, DeepCircularSurface(Vector3(4786f, 3464.4453f, lower), radius)), //S + GantryDenialField(obbasemesh, 3, DeepCircularSurface(Vector3(4846f, 3547.6016f, upper), radius)), //N + GantryDenialField(obbasemesh, 4, DeepCircularSurface(Vector3(4857.5234f, 3536f, upper), radius)), //E + GantryDenialField(obbasemesh, 7, DeepCircularSurface(Vector3(4774.3516f, 3476f, upper), radius)), //W + GantryDenialField(obbasemesh, 8, DeepCircularSurface(Vector3(4786f, 3464.4453f, upper), radius)) //S + ) + } +} + diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index f42442086..92d17f65c 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -40,13 +40,17 @@ import net.psforever.actors.zone.ZoneActor import net.psforever.objects.avatar.Avatar import net.psforever.objects.geometry.Geometry3D import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.UtilityType import net.psforever.objects.vital.etc.{EmpReason, ExplodingEntityReason} -import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.prop.DamageWithPosition +import net.psforever.objects.vital.Vitality +import net.psforever.services.Service /** * A server object representing the one-landmass planets as well as the individual subterranean caverns.
@@ -647,6 +651,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { case (None, _) | (_, None) => ; //let ZoneActor's sanity check catch this error } }) + //doors with nearby locks use those locks as their unlocking mechanism + //let ZoneActor's sanity check catch missing entities + map.doorToLock + .map { case(doorGUID: Int, lockGUID: Int) => (guid(doorGUID), guid(lockGUID)) } + .collect { case (Some(door: Door), Some(lock: IFFLock)) => + door.Actor ! Door.UpdateMechanism(IFFLock.testLock(lock)) + } //ntu management (eventually move to a generic building startup function) buildings.values .flatMap(_.Amenities.filter(_.Definition == GlobalDefinitions.resource_silo)) @@ -661,6 +672,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { case painbox: Painbox => painbox.Actor ! "startup" } + //the orbital_buildings in sanctuary zones have to establish their shuttle routes + map.shuttleBays + .map { guid(_) } + .collect { case Some(obj: OrbitalShuttlePad) => + obj.Actor ! Service.Startup() + } //allocate soi information soi ! SOI.Build() } @@ -924,6 +941,10 @@ object Zone { final case class Despawn(vehicle: Vehicle) + final case class HasSpawned(zone: Zone, vehicle: Vehicle) + + final case class HasDespawned(zone: Zone, vehicle: Vehicle) + final case class CanNotSpawn(zone: Zone, vehicle: Vehicle, reason: String) final case class CanNotDespawn(zone: Zone, vehicle: Vehicle, reason: String) @@ -1247,6 +1268,7 @@ object Zone { def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { distanceCheck(obj1.Definition.Geometry(obj1), obj2.Definition.Geometry(obj2), maxDistance) } + /** * Two game entities are considered "near" each other if they are within a certain distance of one another. * @param g1 the geometric representation of a game entity diff --git a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 47a755e7a..63e34e28e 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -33,12 +33,13 @@ class ZoneMap(val name: String) { var checksum: Long = 0 var zipLinePaths: List[ZipLinePath] = List() var cavern: Boolean = false - var environment: List[PieceOfEnvironment] = List() + var environment: List[PieceOfEnvironment] = List() private var linkTurretWeapon: Map[Int, Int] = Map() private var linkTerminalPad: Map[Int, Int] = Map() private var linkTerminalInterface: Map[Int, Int] = Map() private var linkDoorLock: Map[Int, Int] = Map() private var linkObjectBase: Map[Int, Int] = Map() + private var containsShuttle: List[Int] = List() private var buildings: Map[(String, Int, Int), FoundationBuilder] = Map() private var lattice: Set[(String, String)] = Set() @@ -116,6 +117,12 @@ class ZoneMap(val name: String) { linkTurretWeapon = linkTurretWeapon ++ Map(turretGuid -> weaponGuid) } + def shuttleBays: List[Int] = containsShuttle + + def linkShuttleToBay(shuttleBayGuid: Int): Unit = { + containsShuttle = containsShuttle :+ shuttleBayGuid + } + def latticeLink: Set[(String, String)] = lattice def addLatticeLink(source: String, target: String): Unit = { diff --git a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala index a70b18459..3c73b4162 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala @@ -44,6 +44,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act vehicle.Actor = context.actorOf(Props(classOf[VehicleControl], vehicle), PlanetSideServerObject.UniqueActorName(vehicle)) } + sender() ! Zone.Vehicle.HasSpawned(zone, vehicle) case Zone.Vehicle.Despawn(vehicle) => ZoneVehicleActor.recursiveFindVehicle(vehicleList.iterator, vehicle) match { @@ -51,6 +52,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act vehicleList.remove(index) context.stop(vehicle.Actor) vehicle.Actor = Default.Actor + sender() ! Zone.Vehicle.HasDespawned(zone, vehicle) case None => ; sender() ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find") } diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index ad006ed21..b56d667fa 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -401,16 +401,16 @@ object GamePacketOpcode extends Enumeration { case 0x50 => game.TargetingInfoMessage.decode case 0x51 => game.TriggerEffectMessage.decode case 0x52 => game.WeaponDryFireMessage.decode - case 0x53 => noDecoder(DroppodLaunchRequestMessage) + case 0x53 => game.DroppodLaunchRequestMessage.decode case 0x54 => game.HackMessage.decode - case 0x55 => noDecoder(DroppodLaunchResponseMessage) + case 0x55 => game.DroppodLaunchResponseMessage.decode case 0x56 => game.GenericObjectActionMessage.decode case 0x57 => game.AvatarVehicleTimerMessage.decode // 0x58 case 0x58 => game.AvatarImplantMessage.decode case 0x59 => noDecoder(UnknownMessage89) case 0x5a => game.DelayedPathMountMsg.decode - case 0x5b => noDecoder(OrbitalShuttleTimeMsg) + case 0x5b => game.OrbitalShuttleTimeMsg.decode case 0x5c => noDecoder(AIDamage) case 0x5d => game.DeployObjectMessage.decode case 0x5e => game.FavoritesRequest.decode diff --git a/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala b/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala index c683e8edc..aa57a4d55 100644 --- a/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala +++ b/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala @@ -3,10 +3,32 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} import net.psforever.types.{Angular, PlanetSideGUID, Vector3} +import scodec.Attempt.Successful import scodec.Codec import scodec.codecs._ import shapeless.{::, HNil} +/** + * Dispatched by the server to trigger a droppod's traditional behavior of plummeting from lower orbit like a rock and + * slowing to a gentle land, breaking apart like flower petals to introduce a soldier to the battlefield.
+ *
+ * Only works on droppod-type vehicles. + * Only works if a client avatar is mounted in the vehicle. + * The furthest the vehicle will fall is determined by that avatar player's interaction with the ground. + * The camera is maneuvered in three ways - + * where it starts, + * where it tracks the falling vehicle, + * where it zooms in upon landing. + * Only the "where it starts" portion of the camera is slightly manipulable. + * @param guid the global unique identifier of the droppod + * @param pos the position of the droppod + * @param vel how quickly the droppod is moving + * @param pos2 suggestion for positioning external viewpoint while observing the droppod descending; + * the most common offset from the model position was `Vector3(-20, 1.156f, -50)` + * @param orientation1 na; + * the y-component is usually 70.3125f + * @param orientation2 na + */ final case class DroppodFreefallingMessage( guid: PlanetSideGUID, pos: Vector3, @@ -21,25 +43,23 @@ final case class DroppodFreefallingMessage( } object DroppodFreefallingMessage extends Marshallable[DroppodFreefallingMessage] { + private val rotation: Codec[Vector3] = ( + Angular.codec_roll :: + Angular.codec_pitch :: + Angular.codec_yaw() + ).narrow[Vector3]( + { + case u :: v :: w :: HNil => Successful(Vector3(u, v, w)) + }, + v => v.x :: v.y :: v.z :: HNil + ) + implicit val codec: Codec[DroppodFreefallingMessage] = ( ("guid" | PlanetSideGUID.codec) :: - ("pos" | Vector3.codec_float) :: - ("vel" | Vector3.codec_float) :: - ("pos2" | Vector3.codec_float) :: - ("unkA" | Angular.codec_roll) :: - ("unkB" | Angular.codec_pitch) :: - ("unkC" | Angular.codec_yaw()) :: - ("unkD" | Angular.codec_roll) :: - ("unkE" | Angular.codec_pitch) :: - ("unkF" | Angular.codec_yaw()) - ).xmap[DroppodFreefallingMessage]( - { - case guid :: pos :: vel :: pos2 :: uA :: uB :: uC :: uD :: uE :: uF :: HNil => - DroppodFreefallingMessage(guid, pos, vel, pos2, Vector3(uA, uB, uC), Vector3(uD, uE, uF)) - }, - { - case DroppodFreefallingMessage(guid, pos, vel, pos2, Vector3(uA, uB, uC), Vector3(uD, uE, uF)) => - guid :: pos :: vel :: pos2 :: uA :: uB :: uC :: uD :: uE :: uF :: HNil - } - ) + ("pos" | Vector3.codec_float) :: + ("vel" | Vector3.codec_float) :: + ("pos2" | Vector3.codec_float) :: + ("orientation1" | rotation) :: + ("orientation2" | rotation) + ).as[DroppodFreefallingMessage] } diff --git a/src/main/scala/net/psforever/packet/game/DroppodLaunchInfo.scala b/src/main/scala/net/psforever/packet/game/DroppodLaunchInfo.scala new file mode 100644 index 000000000..72e73d225 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/DroppodLaunchInfo.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.Attempt.Successful +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Information related to this droppod event. + * @see `DroppodLaunchRequestMessage` + * @see `DroppodLaunchResponseMessage` + * @param guid the player using the droppod + * @param zone_number the zone to which the player desires transportation + * @param xypos where in the zone (relative to the ground) the player will be placed + */ +final case class DroppodLaunchInfo( + guid: PlanetSideGUID, + zone_number: Int, + xypos: Vector3 + ) + +object DroppodLaunchInfo { + val codec: Codec[DroppodLaunchInfo] = ( + ("guid" | PlanetSideGUID.codec) :: + ("zone_number" | uint16L) :: + (floatL :: floatL).narrow[Vector3]( + { + case x :: y :: HNil => Successful(Vector3(x, y, 0)) + }, + { + case Vector3(x, y, _) => x :: y :: HNil + } + ) + ).as[DroppodLaunchInfo] +} diff --git a/src/main/scala/net/psforever/packet/game/DroppodLaunchRequestMessage.scala b/src/main/scala/net/psforever/packet/game/DroppodLaunchRequestMessage.scala new file mode 100644 index 000000000..bfab848b9 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/DroppodLaunchRequestMessage.scala @@ -0,0 +1,47 @@ +// 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._ + +/** + * Dispatched from the client to indicate the player wishes to use an orbital droppod + * to rapidly deploy into a zone at a pre-approved position.
+ *
+ * Follows after an instance of "player stasis" where they are permitted to make this sort of selection + * by referencing a zone from the interstellar deployment map. + * This is the conclusion of utilizing the high altitude rapid transport (HART) system + * though does not need to be limited only to prior use of the orbital shuttle. + * @see `PlayerStasisMessage` + * @param info information related to this droppod event + * @param unk na; + * consistently 3 + */ +final case class DroppodLaunchRequestMessage( + info: DroppodLaunchInfo, + unk: Int + ) extends PlanetSideGamePacket { + type Packet = DroppodLaunchRequestMessage + def opcode = GamePacketOpcode.DroppodLaunchRequestMessage + def encode = DroppodLaunchRequestMessage.encode(this) +} + +object DroppodLaunchRequestMessage extends Marshallable[DroppodLaunchRequestMessage] { + /** + * Overloaded constructor that ignores the last field. + * Existing fields match `DroppodLaunchInfo`. + * @param guid the player using the droppod + * @param zoneNumber the zone to which the player desires transportation + * @param pos where in the zone (relative to the ground) the player will be placed + * @return a `DroppodLaunchRequestMessage` packet + */ + def apply(guid: PlanetSideGUID, zoneNumber: Int, pos: Vector3): DroppodLaunchRequestMessage = + DroppodLaunchRequestMessage(DroppodLaunchInfo(guid, zoneNumber, pos), 3) + + implicit val codec: Codec[DroppodLaunchRequestMessage] = ( + DroppodLaunchInfo.codec :: + ("unk" | uint2) + ).as[DroppodLaunchRequestMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/DroppodLaunchResponseMessage.scala b/src/main/scala/net/psforever/packet/game/DroppodLaunchResponseMessage.scala new file mode 100644 index 000000000..10bc1038d --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/DroppodLaunchResponseMessage.scala @@ -0,0 +1,178 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import enumeratum.values.{IntEnum, IntEnumEntry} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.Codec +import scodec.codecs._ + +/** + * The types of errors that can be reported when attempting to droppod into a zone.
+ *
+ * All codes show the preceding text in the events chat window. + * The typo in the message from `BlockedBySOI` can not be resolved by populating any of the greater packet's fields. + * `ZoneFullWarpQueue` utilizes the additional packet fields to establish the warp queue prompt + * with the warp queue and the player's position in that queue. + * The zone to which the player desires transportation is defined elsewhere in the greater packet. + */ +sealed abstract class DroppodError(val value: Int, val message: String) extends IntEnumEntry + +object DroppodError extends IntEnum[DroppodError] { + val values = findValues + + case object ContinentNotAvailable extends DroppodError( + value = 1, + message = "That continent is not available - please choose another one." + ) + + case object BlockedBySOI extends DroppodError( + value = 2, + message = "That location is within a 's Sphere of Influence (SOI). Please try another location." //typo intentional + ) + + case object InvalidLocation extends DroppodError( + value = 3, + message = "That is an invalid drop location - please try another location." + ) + + case object ZoneNotAvailable extends DroppodError( + value = 4, + message = "This zone is not available - try another zone." + ) + + case object ZoneFull extends DroppodError( + value = 5, + message = "That zone is already full of battle hungry people - try another one." + ) + + case object EnemyBase extends DroppodError( + value = 6, + message = "You can not drop onto an enemy home base - please choose a valid continent." + ) + + case object NotOnHart extends DroppodError( + value = 7, + message = "You are attempting to drop but are not on the HART - be warned you are being watched." + ) + + case object OwnFactionLocked extends DroppodError( + value = 8, + message = "You cannot drop onto a continent that is locked to your empire - please choose a valid continent." + ) + + case object ZoneFullWarpQueue extends DroppodError( + value = 9, + message = "The zone you are trying to warp to is currently full. You have been placed in the warp queue." + ) +} + +/** + * Information displayed on the zone warp queue in terms of queue size and queue progression. + * @param queue_size the number of players trying to warp to this zone in the queue ('b' if a/b) + * @param place the player's spot in the queue ('a' if a/b) + */ +final case class WarpQueuePrompt(queue_size: Long, place: Long) + +/** + * Dispatched from the client to indicate the player wished to use an orbital droppod + * but the player will be denied that request for a specific reason. + * The reason manifests as text appended to the event chat window. + * Occasionally, a supplemental window will open with additional information about a delayed action (warp queue). + * @see `DroppodLaunchInfo` + * @param error_code the error reporting why the zoning through droppod use failed + * @param launch_info information related to this droppod event + * @param queue_info if the error invokes the warp queue, the current information about the state of the queue + * @throws AssertionError if the error code requires additional fields + */ +final case class DroppodLaunchResponseMessage( + error_code: DroppodError, + launch_info: DroppodLaunchInfo, + queue_info: Option[WarpQueuePrompt] + ) extends PlanetSideGamePacket { + assert( + error_code != DroppodError.ZoneFullWarpQueue || queue_info.isDefined, + "ZoneFullWarpQueue requires queue information" + ) + type Packet = DroppodLaunchResponseMessage + def opcode = GamePacketOpcode.DroppodLaunchResponseMessage + def encode = DroppodLaunchResponseMessage.encode(this) +} + +object DroppodLaunchResponseMessage extends Marshallable[DroppodLaunchResponseMessage] { + /** + * Overloaded constructor for most errors. + * @param error the error reporting why the zoning through droppod use failed + * @param guid the player using the droppod + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(error: DroppodError, guid: PlanetSideGUID): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage(error, guid, 0, Vector3.Zero) + } + + /** + * Overloaded constructor for most errors. + * @param error the error reporting why the zoning through droppod use failed + * @param guid the player using the droppod + * @param zoneNumber the zone to which the player desires transportation + * @param xypos where in the zone (relative to the ground) the player will be placed + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(error: DroppodError, guid: PlanetSideGUID, zoneNumber: Int, xypos: Vector3): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage(error, DroppodLaunchInfo(guid, zoneNumber, xypos)) + } + + /** + * Overloaded constructor for quickly reflecting errors. + * @param error the error reporting why the zoning through droppod use failed + * @param info information related to this droppod event + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(error: DroppodError, info: DroppodLaunchInfo): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage(error, info, None) + } + + /** + * Overloaded constructor for `ZoneFullWarpQueue` errors. + * @param guid the player using the droppod + * @param zoneNumber the zone to which the player desires transportation + * @param queueSize the number of players trying to warp to this zone in the queue ('b' if a/b) + * @param placeInQueue the player's spot in the queue ('a' if a/b) + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(guid: PlanetSideGUID, zoneNumber: Int, queueSize: Int, placeInQueue: Int): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage( + DroppodLaunchInfo(guid, zoneNumber, Vector3.Zero), + queueSize, placeInQueue + ) + } + + /** + * Overloaded constructor for quickly reflecting `ZoneFullWarpQueue` errors. + * @param info information related to this droppod event + * @param queueSize the number of players trying to warp to this zone in the queue ('b' if a/b) + * @param placeInQueue the player's spot in the queue ('a' if a/b) + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(info: DroppodLaunchInfo, queueSize: Int, placeInQueue: Int): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage( + DroppodError.ZoneFullWarpQueue, + info, + Some(WarpQueuePrompt(queueSize, placeInQueue)) + ) + } + + private val droppodErrorCodec: Codec[DroppodError] = PacketHelpers.createIntEnumCodec(DroppodError, uint4) + + private val extra_codec: Codec[WarpQueuePrompt] = ( + ("place" | uint32L) :: + ("queue_size" | uint32L) + ).as[WarpQueuePrompt] + + implicit val codec: Codec[DroppodLaunchResponseMessage] = ( + ("error_code" | droppodErrorCodec) >>:~ { ecode => + ("launch_info" | DroppodLaunchInfo.codec) :: + ("queue_info" | conditional(ecode == DroppodError.ZoneFullWarpQueue, extra_codec)) + } + ).as[DroppodLaunchResponseMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala index a7b60f0af..3b6ae389e 100644 --- a/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala @@ -39,6 +39,7 @@ import scodec.codecs._ * 16 - Max unanchor * 20 - Client requests MAX special effect (NC shield and TR overdrive. VS jump jets are handled by the jump_thrust boolean on PlayerStateMessageUpstream) * 21 - Disable MAX special effect (NC shield) + * 28 - Cancel warp queue (see: `DroppodLaunchResponseMessage`)
* 29 - AFK
* 30 - back in game
* 36 - turn on "Looking for Squad"
diff --git a/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala b/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala index 2084239c4..5f9901099 100644 --- a/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala +++ b/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala @@ -12,13 +12,13 @@ import scodec.codecs._ * The client will only dispatch this packet when it feels confident that the player can get into a vehicle. * It makes its own check whether or not to display that "enter vehicle here" icon on the ground. * This is called an "entry point." - * Entry points and seat numbers are not required as one-to-one; - * multiple entry points can lead to the same seat, such as the driver seat of an ANT.
+ * Entry points and mount numbers are not required as one-to-one; + * multiple entry points can lead to the same mount, such as the driver mount of an ANT.
*
* The player is not allowed to board anything until the server responds in affirmation. * @param player_guid the player * @param vehicle_guid the vehicle - * @param entry_point the entry index that maps to a seat index, specific to the selected vehicle + * @param entry_point the entry index that maps to a mount index, specific to the selected vehicle */ final case class MountVehicleMsg(player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID, entry_point: Int) extends PlanetSideGamePacket { diff --git a/src/main/scala/net/psforever/packet/game/OrbitalShuttleTimeMsg.scala b/src/main/scala/net/psforever/packet/game/OrbitalShuttleTimeMsg.scala new file mode 100644 index 000000000..f02d1010d --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OrbitalShuttleTimeMsg.scala @@ -0,0 +1,125 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.{HartSequence, PlanetSideGUID} +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Paired globally unique identifier numbers, + * the first one being the pad (`obbasemesh`) of a HART shuttle building, + * the second being the shuttle itself. + * @param pad the HART shuttle pad + * @param shuttle the HART orbital shuttle + * @param unk a control code; + * has indeterminate purpose regardless of the phase expressed in the greater packet; + * frequently `20` but also frequently varies + */ +final case class PadAndShuttlePair(pad: PlanetSideGUID, shuttle: PlanetSideGUID, unk: Int) + +/** + * Control the animation state transitions of the high altitude rapid transport (HART) orbital shuttle building + * and the accompanying orbital shuttle model.
+ *
+ * The animation sequence is controlled primarily by the first field and + * goes through a strict cycle of boarding, lift shuttle, takeoff, land, lower shuttle. + * All HART facilities (amenity `obbasemesh`) in a given zone are controlled by this packet. + * Multiple systems are controlled by a single field during a given animation, + * e.g., the boarding gantries are retracted or extended during the same part where the shuttle is raised or lowered. + * Certain neutral animation states - `State0`, `State5`, and `State7` - all behave the same way + * though denote different points in the sequence. + * Animation subsequence states are coordinated by the second field, + * though the specific purpose of the subsequence isn't always obvious, + * and the field isn't always necessary to achieve the result of the primary sequence.
+ *
+ * The total time of the system is bound between two states: + * whether the shuttle has left or whether it is boarding. + * When separated ("has left"), + * the shuttle will be lifted out of the bay to atop the building and will fly off into the horizon, + * remaining despawned until it returns to view, perches atop the building again, and is lowered into the bay. + * When boarding, + * the shuttle is fixed in the bay and is accepting passengers via one of the boarding hallways. + * Upon boarding the shuttle, the time until takeoff ("has left") is displayed to all waiting passengers + * in the form of a progress bar. + * This progress bar is fixed to a full time of 60 seconds (60000 milliseconds) in the client and + * will start at fractions of completion for boarding times under 60 seconds.
+ *
+ * Pairs of globally unique identifiers for the shuttle facility and the shuttle + * link the time fields to their function. + * All facilities and shuttles in a given zone are paired and enumerated for a single packet. + * If the HART facility identifier is missing or incorrect, + * the absent facility will continue to undergo correct animation state transition, + * but the door timer will not animate correctly and constantly display the time 10:37 and + * the door lights will be neither locked closed (red) or openable (green). + * If the shuttle identifier is missing or incorrect, + * the absent shuttle will continue to undergo partially correct animation state transitions, + * cycling between visible and invisible atop the HART facility, + * and the aforementioned progress bars visible by shuttle passengers will not display during the boarding phase + * if the shuttle is made available for boarding. + * @param model_state a control code that affects the over-all state of the HART system + * @param unk0 na + * @param arrival_time the time for the orbital shuttle to return during instances when the shuttle is away; + * displayed on a related time near the shuttle boarding entryways; + * in milliseconds + * @param boarding_time the time for the orbital shuttle to depart during instances when the shuttle is boarding; + * frequently `8000L` when not in use; + * in milliseconds + * @param other_time time field used for a variety of things; + * in most uses, the amount of time that has passed since the start of the event, + * so usually `0` (at start of event); + * with respects to `model_state` and `unk3`: + * full departure time when `5`-`3` (variant of `7`-`3`); + * occasionally, full departure time when `0`-`0` + * in milliseconds + * @param pairs a list of entries that pair + * a paired facility pad unique identifier and shuttle unique identifier + * with a control code + */ +final case class OrbitalShuttleTimeMsg( + model_state: HartSequence, + unk0: Int, + arrival_time: Long, + boarding_time: Long, + other_time: Long, + pairs: List[PadAndShuttlePair] + ) + extends PlanetSideGamePacket { + type Packet = OrbitalShuttleTimeMsg + def opcode = GamePacketOpcode.OrbitalShuttleTimeMsg + def encode = OrbitalShuttleTimeMsg.encode(this) +} + +object OrbitalShuttleTimeMsg extends Marshallable[OrbitalShuttleTimeMsg] { + private val uint3: Codec[Int] = uint(bits = 3) + + private val hartSequenceCodec: Codec[HartSequence] = PacketHelpers.createIntEnumCodec(HartSequence, uint3) + + private val padShuttlePair_codec: Codec[PadAndShuttlePair] = ( + ("pad" | PlanetSideGUID.codec) :: + ("shuttle" | PlanetSideGUID.codec) :: + ("unk" | uint(bits = 6)) + ).as[PadAndShuttlePair] + + implicit val codec: Codec[OrbitalShuttleTimeMsg] = ( + uint3 >>:~ { size => + ("model_state" | hartSequenceCodec) :: + ("unk0" | uint3) :: + ("arrival_time" | uint32L) :: + ("boarding_time" | uint32L) :: + bool :: + ("other_time" | uint32L) :: + ("pairs" | PacketHelpers.listOfNSized(size, padShuttlePair_codec)) + } + ).xmap[OrbitalShuttleTimeMsg]( + { + case _ :: model :: u0 :: arrival :: boarding :: _ :: other :: pairs :: HNil => + OrbitalShuttleTimeMsg(model, u0, arrival, boarding, other, pairs) + }, + { + case OrbitalShuttleTimeMsg(model, u0, arrival, boarding, other, pairs) => + pairs.length :: model :: u0 :: arrival :: boarding :: true :: other :: pairs :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index a89222147..6a3770b18 100644 --- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -179,14 +179,14 @@ import scodec.codecs._ * `228 - Player/vehicle leaves black ops`
*
* `Vehicles:`
- * `10 - Driver seat permissions` + * `10 - Driver mount permissions` * - * `11 - Gunner seat(s) permissions (same)`
- * `12 - Passenger seat(s) permissions (same)`
+ * `11 - Gunner mount(s) permissions (same)`
+ * `12 - Passenger mount(s) permissions (same)`
* `13 - Trunk permissions (same)`
* `21 - Declare a player the vehicle's owner, by globally unique identifier`
* `22 - Toggles gunner and passenger mount points (1 = hides, 0 = reveals; this also locks their permissions)`
diff --git a/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala b/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala index faccb900e..a00d1ff82 100644 --- a/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala @@ -15,7 +15,9 @@ import scodec.codecs._ * @param ang the orientation of the vehicle * @param vel optional movement data * @param flying flight information, valid only for a vehicle that can fly when in flight; - * `Some(7)`, when in a flying state (vertical thrust unnecessary to unlock movement) + * `Some(7)`, when in a flying state (vertical thrust unnecessary to unlock movement); + * `Some(10) - Some(15)`, used by the HART during landing and take-off, + * in repeating order: 13, 14, 10, 11, 12, 15; * `None`, when landed and for all vehicles that do not fly * @param unk3 na * @param unk4 na diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala index 98762166f..470b59a80 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala @@ -15,7 +15,7 @@ import shapeless.{::, HNil} * Upon hitting the ground, it opens up, releasing the player, and despawns.
*
* Although the droppod is not technically a vehicle, it is treated as such by the game. - * A spawned and unoccupied droppod can be entered and exited, as expected (the seat is 0). + * A spawned and unoccupied droppod can be entered and exited, as expected (the mount is 0). * There is no entry animation. * The exit animation is the droppod flowering open as usual. * Even in its spread open state, the droppod can be re-entered, though it will remain spread open. diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala index 44dcb2184..2c311853d 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala @@ -55,7 +55,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl * health should be less than 3/255, or 0%
* -jammered - vehicles will not be jammered by setting this field
* -player_guid the vehicle's (official) owner; - * a living player in the game world on the same continent as the vehicle who may mount the driver seat + * a living player in the game world on the same continent as the vehicle who may mount the driver mount * @param unk3 na * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) * @param unk4 na @@ -69,7 +69,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl * see `vehicle_type` * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included; * will also include trunk contents; - * the driver is the only valid seat entry (more will cause the access permissions to act up) + * the driver is the only valid mount entry (more will cause the access permissions to act up) * @param vehicle_type a modifier for parsing the vehicle data format differently; * see `vehicle_format_data`; * defaults to `Normal` @@ -393,8 +393,8 @@ object VehicleData extends Marshallable[VehicleData] { * the entries are temporarily formatted into a linked list before being put back into a normal `List`.
*
* 6 June 2018:
- * Due to curious behavior in the vehicle seat access controls, - * please only encode and decode the driver seat even though all seats are currently reachable. + * Due to curious behavior in the vehicle mount access controls, + * please only encode and decode the driver mount even though all seats are currently reachable. * @param length the distance in bits to the first inventory entry * @return a `Codec` that translates `InventoryData` */ @@ -404,8 +404,8 @@ object VehicleData extends Marshallable[VehicleData] { uint8 >>:~ { size => uint2 :: (inventory_seat_codec( - length, //length of stream until current seat - CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next seat + length, //length of stream until current mount + CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next mount ) >>:~ { seats => PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist }) @@ -450,13 +450,13 @@ object VehicleData extends Marshallable[VehicleData] { conditional( objClass == ObjectClass.avatar, inventory_seat_codec( - { //length of stream until next seat + { //length of stream until next mount length + (seat match { case Some(o) => o.bitsize case None => 0 }) }, - CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next seat + CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next mount ) ).hlist } @@ -487,7 +487,7 @@ object VehicleData extends Marshallable[VehicleData] { * The operation performed by this `Codec` is very similar to `InternalSlot.codec`. * @param pad the padding offset for the player's name; * 0-7 bits; - * this padding value must recalculate for each represented seat + * this padding value must recalculate for each represented mount * @see `CharacterAppearanceData`
* `VehicleData.InitialStreamLengthToSeatEntries`
* `CumulativeSeatedPlayerNamePadding` diff --git a/src/main/scala/net/psforever/services/InterstellarClusterService.scala b/src/main/scala/net/psforever/services/InterstellarClusterService.scala index c8ed4d95a..3e6133e2e 100644 --- a/src/main/scala/net/psforever/services/InterstellarClusterService.scala +++ b/src/main/scala/net/psforever/services/InterstellarClusterService.scala @@ -8,8 +8,10 @@ import net.psforever.objects.avatar.Avatar import net.psforever.objects.{Player, SpawnPoint, Vehicle} import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.zones.Zone +import net.psforever.packet.game.DroppodError import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3} import net.psforever.util.Config +import net.psforever.zones.Zones import scala.collection.mutable import scala.util.Random @@ -71,6 +73,19 @@ object InterstellarClusterService { final case class GetPlayers(replyTo: ActorRef[PlayersResponse]) extends Command final case class PlayersResponse(players: Seq[Avatar]) + + final case class DroppodLaunchRequest( + zoneNumber: Int, + position: Vector3, + faction: PlanetSideEmpire.Value, + replyTo: ActorRef[DroppodLaunchExchange] + ) extends Command + + trait DroppodLaunchExchange + + final case class DroppodLaunchConfirmation(destination: Zone, position: Vector3) extends DroppodLaunchExchange + + final case class DroppodLaunchDenial(errorCode: DroppodError, data: Option[Any]) extends DroppodLaunchExchange } class InterstellarClusterService(context: ActorContext[InterstellarClusterService.Command], _zones: Iterable[Zone]) @@ -138,7 +153,7 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic Ordering[Int].reverse ) // greatest > least .sortWith { - case ((_, spot1, _), (_, spot2, _)) => + case ((_, spot1, _), (_, _, _)) => spot1.ActivityBy().contains(faction) // prefer own faction activity } .headOption @@ -210,6 +225,22 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic case None => replyTo ! SpawnPointResponse(None) } + + case DroppodLaunchRequest(zoneNumber, position, faction, replyTo) => + zones.find(_.Number == zoneNumber) match { + case Some(zone) => + //TODO all of the checks for the specific DroppodLaunchResponseMessage excuses go here + if(zone.map.cavern) { + //just being cautious - caverns are typically not normally selectable as drop zones + replyTo ! DroppodLaunchDenial(DroppodError.ZoneNotAvailable, None) + } else if (zone.Number == Zones.sanctuaryZoneNumber(faction)) { + replyTo ! DroppodLaunchDenial(DroppodError.OwnFactionLocked, None) + } else { + replyTo ! DroppodLaunchConfirmation(zone, position) + } + case None => + replyTo ! DroppodLaunchDenial(DroppodError.InvalidLocation, None) + } } this diff --git a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala index 1f0c2afb9..aefe374ab 100644 --- a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala +++ b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala @@ -21,13 +21,13 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} * relogging (short-term client connectivity issue resolution), and * logout (end-of-life conditions involving the separation of a user from the game world).
*
- * A user polls this service and the net.psforever.services either creates a new `PersistenceMonitor` entity + * A user polls this service and the service either creates a new `PersistenceMonitor` entity * or returns whatever `PersistenceMonitor` entity currently exists. - * Performing informative pdates to the monitor about the user's eventual player avatar instance + * Performing informative updates to the monitor about the user's eventual player avatar instance * (which can be performed by messaging the service indirectly, * though sending directly to the monitor is recommended) * facilitate the management of persistence. - * If connectivity isssues with the client are encountered by the user, + * If connectivity issues with the client are encountered by the user, * within a reasonable amount of time to connection restoration, * the user may regain control of their existing persistence monitor and, thus, the same player avatar. * End of life is mainly managed by the monitors internally @@ -144,7 +144,7 @@ class AccountPersistenceService extends Actor { */ def CreateNewPlayerToken(name: String): ActorRef = { val ref = - context.actorOf(Props(classOf[PersistenceMonitor], name, squad), s"$name-${NextPlayerIndex(name)}") + context.actorOf(Props(classOf[PersistenceMonitor], name, squad), s"${NextPlayerIndex(name)}_${name.hashCode()}") accounts += name -> ref ref } @@ -312,7 +312,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor { *
* The updates have been providing the zone * and the basic information about the user (player name) has been provided since the beginning - * and it's a trivial matter to find where the avatar and player and asess their circumstances. + * and it's a trivial matter to find where the avatar and player and assess their circumstances. * The four important vectors are: * the player avatar is in a vehicle, * the player avatar is standing, @@ -336,7 +336,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor { case _ => (None, None) //bad data? }) match { case (Some(_), Some(seat)) => - seat.Occupant = None //unseat + seat.unmount(player) //unmount case _ => ; } PlayerAvatarLogout(avatar, player) diff --git a/src/main/scala/net/psforever/services/hart/HartEvent.scala b/src/main/scala/net/psforever/services/hart/HartEvent.scala new file mode 100644 index 000000000..175c8c1d8 --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartEvent.scala @@ -0,0 +1,272 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import net.psforever.types.HartSequence + +/** + * The various `flying` states assigned to the orbital shuttle + * in close to an order in which they are assigned. + */ +object ShuttleState extends Enumeration { + type Type = Value + + val State13 = Value(13) + val State14 = Value(14) + val State10 = Value(10) + val State11 = Value(11) + val State12 = Value(12) + val State15 = Value(15) +} + +/** + * Produce the specific animation sequence and the ???. + * @see `OrbitalShuttleEvent` + * @see `OrbitalShuttleTimeMsg` + * @see `HartEvent` + * @param u1 the animation code for the HART + * @param u2 ??? + */ +final case class HartEventStateFields(u1: HartSequence, u2: Int) + +/** + * Produce the time data of this event in the sequence. + * @see `OrbitalShuttleEvent` + * @see `OrbitalShuttleTimeMsg` + * @see `HartEvent` + * @param t1 in general, time for the shuttle to arrive + * @param t2 in general, `8000L`; + * when being useful, time for the shuttle to board passengers + * @param t3 in general, time elasped + */ +final case class HartEventTimeFields(t1: Long, t2: Long, t3: Long) + +/** + * An event in the sequence of the high-altitude rapid transport (HART) system + * encompassing both ground facility conditions and conditions of the orbital shuttle. + */ +sealed trait HartEvent { + /** HART facility and shuttle animation */ + def u1: HartSequence + /** counter? */ + def u2: Int + /** starting time on the clock; typically seen on the display */ + def timeOnClock: Long + /** for how long this event goes on */ + def duration: Long + /** are the managed doors for the HART facility locked closed; + * this is an active state field: `true` - locked right now and `false` - unlocked right now + */ + def lockedDoors: Boolean = true + /** the shuttle has a unique state to expose to the zone; + * the state is related to a value in the `Flying` field of a `VehicleStateMessage` packet + */ + def shuttleState: Option[ShuttleState.Value] + /** how the shuttle and the HART facility interact; + * this is an active state field: + * `Some(true)` - the shuttle is docked right now; + * `Some(false)` - the shuttle has freed itself from the facility's dock right now; + * `None` - the shuttle is acting freely apart from its facility */ + def docked: Option[Boolean] + /** these fields must be including prior to an update if the shuttle state was not previous known; + * the primary purpose is to place the shuttle platform at the correct elevation + */ + def prerequisiteUpdate: Option[HartEventStateFields] + + /** + * Get the animation state fields for this event. + * @param time during update requests, the amount of time that has elapsed during the start of this event + * @return the animation state data + */ + def stateFields(time: Option[Long] = None): HartEventStateFields = { + HartEventStateFields(u1, u2) + } + + /** + * Get the primary time fields for this event. + * @param time during update requests, the amount of time that has elapsed during the start of this event + * @return the time data + */ + def timeFields(time: Option[Long] = None): HartEventTimeFields = { + HartEventTimeFields( + time match { + case Some(t) if timeOnClock > t => timeOnClock - t + case Some(t) if timeOnClock <= t => 0L + case _ => timeOnClock + }, + 8000L, + time match { + case Some(t) => t + case _ => 0 + } + ) + } +} + +object HartEvent { + private val prepareForDepartureOnUpdate: Option[HartEventStateFields] = + Some(HartEventStateFields(HartSequence.PrepareForDeparture, 1)) + + final case class Boarding(duration: Long) extends HartEvent { + def u1: HartSequence = HartSequence.State0 + def u2: Int = 0 + def timeOnClock: Long = duration + override def lockedDoors: Boolean = false + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State10) + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + + override def timeFields(time: Option[Long]): HartEventTimeFields = { + /* + the full progress bar only displays 60s + for other times, the progress bar will only display the portion necessary to represent the time in respect to 60s + */ + HartEventTimeFields( + 0L, + super.timeFields(time).t1, + time match { + case None => 0L + case Some(_) => timeOnClock + } + ) + } + } + + final case class ShuttleTakeoffOps(timeOnClock: Long) extends HartEvent { + def u1: HartSequence = HartSequence.PrepareForDeparture + def u2: Int = 1 + def duration: Long = 8000 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State11) + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + } + + object ShuttleTakeoffOps { + final val duration: Long = 8000L + } + + final case class Takeoff(timeOnClock: Long) extends HartEvent { + def u1: HartSequence = HartSequence.TakeOff + def u2: Int = 2 + def duration: Long = Takeoff.duration + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State12) + def docked: Option[Boolean] = Some(false) + def prerequisiteUpdate: Option[HartEventStateFields] = prepareForDepartureOnUpdate + } + + object Takeoff { + final val duration: Long = 13300L + } + + final case class InTransit( + timeOnClock: Long, + duration: Long, + boardingDuration: Long + ) extends HartEvent { + def u1: HartSequence = HartSequence.State7 + def u2: Int = 3 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State15) + def docked: Option[Boolean] = None + def prerequisiteUpdate: Option[HartEventStateFields] = None + + override def stateFields(time: Option[Long] = None): HartEventStateFields = { + HartEventStateFields( + time match { + case Some(_) => HartSequence.State5 + case _ => u1 + }, + u2 + ) + } + + override def timeFields(time: Option[Long]): HartEventTimeFields = { + HartEventTimeFields( + time match { + case Some(t) if timeOnClock > t => timeOnClock - t + case Some(t) if timeOnClock <= t => 0L + case _ => timeOnClock + }, + 8000L, + time match { + case Some(_) => boardingDuration + case _ => 0 + } + ) + } + } + + case object Arrival extends HartEvent { + def u1: HartSequence = HartSequence.Land + def u2: Int = 4 + def timeOnClock: Long = 23700 + def duration: Long = 15700 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State13) + def docked: Option[Boolean] = None + def prerequisiteUpdate: Option[HartEventStateFields] = prepareForDepartureOnUpdate + } + + case object ShuttleDockingOps extends HartEvent { + def u1: HartSequence = HartSequence.PrepareForBoarding + def u2: Int = 5 + def timeOnClock: Long = 8000 + def duration: Long = 8000 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State14) + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + } + + case object Blanking extends HartEvent { + def u1: HartSequence = HartSequence.State0 + def u2: Int = 5 + def timeOnClock: Long = 4294967295L + def duration: Long = 1 //for how long? + def shuttleState: Option[ShuttleState.Value] = None + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + + override def timeFields(time: Option[Long]): HartEventTimeFields = + HartEventTimeFields(timeOnClock, 8000L, 0L) + } + + /** + * The high alititude rapid transport (HART) system is centered around a series of animations + * of a component orbital shuttle landing and taking off from a given facility. + * The two important times are the length pof the time the shuttle is away from the facility and + * the length of time that the shuttle is docked at the facility to allow for passenger boarding. + * The sequence progresses through stages from the shuttle being landed, to the shuttle departing, + * to the shuttle returning, and then starting back with the shuttle being landed. + *
+ * As the shuttle animates, the facility also animates. + * As both the shuttle and the facility animate, various other components connect to the facility and to the shuttle + * undergo state changes, allowing or denying access to the shuttle's boarding routines. + * When boarding is permitted, this phase is considered as part of a single event in the sequence, + * and boarding duration lasts for that entire event. + * The remainder of the sequence is devoted to a remainder of time from the other duration + * once the known time of fixed animation events are deducted. + * @param inFlightDuration for how long the orbital shuttle is away from being docked at the HART building + * and not allowing passengers to board + * @param boardingDuration for how long the orbital shuttle is landed at its component HART building + * and is allowing passnegers to board + * @return the final sequence of events + */ + def buildEventSequence(inFlightDuration: Long, boardingDuration: Long): Seq[HartEvent] = { + val returnDurations = Arrival.duration + ShuttleDockingOps.duration + val fixedDurations = ShuttleTakeoffOps.duration + Takeoff.duration + returnDurations + val full = if (inFlightDuration > fixedDurations) { + inFlightDuration + } else { + inFlightDuration + fixedDurations + } + val firstTime = full - ShuttleTakeoffOps.duration + val secondTime = firstTime - Takeoff.duration + val awayDuration = secondTime - returnDurations + Seq( + Boarding(boardingDuration), + ShuttleTakeoffOps(full), + Takeoff(firstTime), + InTransit(secondTime, awayDuration, boardingDuration), + Arrival, + ShuttleDockingOps, + Blanking + ) + } +} diff --git a/src/main/scala/net/psforever/services/hart/HartService.scala b/src/main/scala/net/psforever/services/hart/HartService.scala new file mode 100644 index 000000000..4ecbfb771 --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartService.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import akka.actor.{Actor, ActorRef, Props} +import net.psforever.util.Config + +import scala.collection.concurrent.TrieMap + +/** + * Coordinate the components - facility landing pad and orbital shuttle - + * of the high altitude rapid transport (HART) system for any zone that attempts to register. + * When a pair of staging pad and orbital shuttle attempt to register with the system, + * either locate an existing zone-based manager or create a new manager for this zone, + * and tell that manager that the pair is (now) under its supervision. + * @see `HartTimer` + */ +class HartService extends Actor { + /** key - a zone id; value - the manager for that zone's HART system */ + val zoneTimers: TrieMap[String, ActorRef] = TrieMap[String, ActorRef]() + + def receive: Receive = { + case out : HartTimer.PairWith => + val zone = out.zone + val channel = zone.id + (zoneTimers.get(channel) match { + case Some(o) => + o + case None => + val actor = context.actorOf(Props(classOf[HartTimer], zone), s"$channel-shuttle-timer") + zoneTimers.put(channel, actor) + actor.tell( + HartTimer.SetEventDurations( + channel, + Config.app.game.hart.inFlightDuration, + Config.app.game.hart.boardingDuration + ), + self + ) + actor + }).tell(out, out.from) + + case out: HartTimer.MessageToHartInZone => + zoneTimers.get(out.inZone) match { + case Some(o) => o ! out + case _ => + } + + case _ => ; + } +} diff --git a/src/main/scala/net/psforever/services/hart/HartTimer.scala b/src/main/scala/net/psforever/services/hart/HartTimer.scala new file mode 100644 index 000000000..3de884fe0 --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartTimer.scala @@ -0,0 +1,286 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.Default +import net.psforever.objects.zones.Zone +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.{GenericEventBus, GenericEventBusMsg} +import net.psforever.types.{HartSequence, PlanetSideGUID} + +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +/** + * Within each zone, all high-altitude rapid transport (HART) systems are controlled in unison. + * A HART system is composed of a facility (amenity) that embodies passenger onboarding services + * and a semi-interactive shuttle that gateways to the orbital droppod system. + * Provide supervision to these components by managing the over-all HART sequence. + * @param zone the zone being represented by this particular HART service + */ +class HartTimer(zone: Zone) extends Actor { + /** since the system is zone-locked, caching this value is fine */ + val zoneId = zone.id + /** all of the paired HART facility amenities and the shuttle housed in that facility (in that order) */ + var padAndShuttlePairs: List[(PlanetSideGUID, PlanetSideGUID)] = List() + + /* the HART system is controlled by a sequence of events; + * the sequence describes key state changes and animation cues + * to produce the effect of the orbital shuttle being used + */ + var sequence = Seq.empty[HartEvent] + /** index keeping track of the current event in the sequence */ + var sequenceIndex: Int = 0 + /** how many events are a part of this sequence */ + var sequenceLength = 0 + /** when the timing of the events in the system changes, + * do not push the changes until completion of the current routine + */ + var delayedScheduleChange: Option[Seq[HartEvent]] = None + /** the time at the start of the previous event */ + var lastStartTime: Long = 0 + /** scheduler for each event in the sequence */ + var timer: Cancellable = Default.Cancellable + + /** a message bus to which all associated orbital shuttle pads are subscribed */ + val padEvents = new GenericEventBus[HartTimer.Command] + /** cache common messages */ + val shuttleDockedInThisZone = HartTimer.ShuttleDocked(zoneId) + val shuttleFreeFromDockInThisZone = HartTimer.ShuttleFreeFromDock(zoneId) + + /** the behaviors common to both the inert and active operations of the hart */ + val commonBehavior: Receive = { + case HartTimer.SetEventDurations(_, awayDuration: Long, boardingDuration: Long) => + val newSequence = HartEvent.buildEventSequence(awayDuration, boardingDuration) + if (newSequence.nonEmpty) { + if (timer.isCancelled) { + sequence = newSequence + sequenceLength = newSequence.length + nextEvent(sequenceIndex) + } else { + delayedScheduleChange = Some(newSequence) + } + context.become(flightsScheduled) + } + } + + /** behaviors that are valid while no sequence of events is defined; the hart is inert */ + def grounded: Receive = commonBehavior + .orElse { + case HartTimer.PairWith(_, pad, shuttle, from) => + pairWith(pad, shuttle, from) + + case _ => ; + } + + /** behaviors that are valid after a sequence of events is defined; the hart is active */ + def flightsScheduled: Receive = commonBehavior + .orElse { + case HartTimer.PairWith(_, pad, shuttle, from) => + pairWith(pad, shuttle, from) + val event = sequence(sequenceIndex) + if (event.lockedDoors) { + from ! HartTimer.LockDoors + } + if (event.docked.contains(true)) { + from ! HartTimer.ShuttleDocked(zoneId) + } + + case HartTimer.NextEvent(next) if next == 0 => + sequence = delayedScheduleChange.getOrElse(sequence) + sequenceLength = sequence.length + delayedScheduleChange = None + nextEvent(next) + + case HartTimer.NextEvent(next) => + nextEvent(next) + + case HartTimer.Update(_, forChannel) => + val seq = sequence + val event = seq(sequenceIndex) + val time = Some(System.currentTimeMillis() - lastStartTime) + if (event.docked.contains(true)) { + padEvents.publish( HartTimer.ShuttleDocked(forChannel) ) + } + event.prerequisiteUpdate match { + case Some(fields) => + val times = event.timeFields(time) + zone.LocalEvents ! LocalServiceMessage( + forChannel, + LocalAction.ShuttleEvent(HartTimer.OrbitalShuttleEvent( + fields.u1, fields.u2, times.t1, times.t2, times.t3, padAndShuttlePairs zip Seq(20, 20, 20) + )) + ) + case None => ; + } + zone.LocalEvents ! LocalServiceMessage( + forChannel, + LocalAction.ShuttleEvent( + HartTimer.analyzeEvent(event, padAndShuttlePairs, time) + ) + ) + event.shuttleState match { + case Some(state) => + padEvents.publish( HartTimer.ShuttleStateUpdate(forChannel, state.id) ) + case None => + //find previous valid shuttle state + var i = sequenceIndex - 1 + while(seq(i).shuttleState.isEmpty) { i = if (i - 1 < 0) sequenceLength - 1 else i - 1 } + padEvents.publish( HartTimer.ShuttleStateUpdate(forChannel, seq(i).shuttleState.get.id) ) + } + + case _ => ; + } + + def receive: Receive = grounded + + def pairWith(pad: PlanetSideGUID, shuttle: PlanetSideGUID, from: ActorRef): Unit = { + padEvents.subscribe(from, to = "") + padAndShuttlePairs = (padAndShuttlePairs :+ (pad, shuttle)).distinct + } + + def nextEvent(next: Int): Unit = { + val currEvent = sequence(sequenceIndex) + val event = sequence(next) + sequenceIndex = next + lastStartTime = System.currentTimeMillis() + timer = context.system.scheduler.scheduleOnce( + event.duration milliseconds, + self, + HartTimer.NextEvent((next + 1) % sequenceLength) + ) + //updates + val evt = HartTimer.analyzeEvent(event, padAndShuttlePairs) + event.docked match { + case Some(true) if currEvent.docked.isEmpty => + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.ShuttleEvent(evt)) + padEvents.publish( shuttleDockedInThisZone ) + case Some(false) if currEvent.docked.contains(true) => + padEvents.publish( shuttleFreeFromDockInThisZone ) + context.system.scheduler.scheduleOnce( + delay = 10 milliseconds, + zone.LocalEvents, + LocalServiceMessage(zoneId, LocalAction.ShuttleEvent(evt)) + ) + case _ => + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.ShuttleEvent(evt)) + } + if (currEvent.lockedDoors != event.lockedDoors) { + padEvents.publish( if(event.lockedDoors) HartTimer.LockDoors else HartTimer.UnlockDoors ) + } + event.shuttleState match { + case Some(state) => + padEvents.publish( HartTimer.ShuttleStateUpdate(zoneId, state.id) ) + case None => ; + } + } +} + +object HartTimer { + /** + * Transform `HartEvent` data into `OrbitalShuttleEvent` data. + * The former is treated as something internal. + * The latter is treated as something external. + * @see `OrbitalShuttleEvent` + * @see `HartEvent` + * @param event the `TimeShuttleEvent` data + * @param time how long has the current event in th sequence been occurring + * @return the `OrbitalShuttleEvent` data + */ + def analyzeEvent( + event: HartEvent, + padAndShuttlePairs: List[(PlanetSideGUID, PlanetSideGUID)], + time: Option[Long] = None + ): OrbitalShuttleEvent = { + import net.psforever.services.hart.HartEvent._ + val stateFields = event.stateFields(time) + val timeFields = event.timeFields(time) + //these control codes are taken from packets samples for VS sanctuary during a specific few sequences + //while the number varies - from 5 to 37 and an actual maximum of 63 - their purpose seems indeterminate + val pairs = event match { + case _: Boarding => Seq(20, 20, 20) + case _: ShuttleTakeoffOps => Seq(20, 20, 20) + case _: Takeoff => Seq( 6, 25, 5) + case _: InTransit => Seq(20, 20, 20) + case Arrival => Seq( 5, 5, 27) + case ShuttleDockingOps => Seq(20, 20, 20) + case Blanking => Seq(20, 20, 20) + case _ => Seq(20, 20, 20) + } + OrbitalShuttleEvent( + stateFields.u1, stateFields.u2, + timeFields.t1, timeFields.t2, timeFields.t3, + padAndShuttlePairs zip pairs + ) + } + + /** + * Internal message to advance the sequence event. + * @param index the position of the next event + */ + private case class NextEvent(index: Int) + + trait MessageToHartInZone { + def inZone: String + } + + /** + * Personalized messages that align the state of the shuttle to one's perspective (client). + * @param inZone the zone for which the update will be composed + * @param forChannel to whom to address the reply + */ + final case class Update(inZone: String, forChannel: String) extends MessageToHartInZone + + final case class SetEventDurations(inZone: String, away: Long, boarding: Long) extends MessageToHartInZone + /** + * Append information about a building amenity and shuttle combination in this zone. + * @param zone the relevant zone + * @param pad the orbital shuttle pad (`obbasemesh`) + * @param shuttle the orbital shuttle + * @param from the control agency of the pad + */ + final case class PairWith(zone: Zone, pad: PlanetSideGUID, shuttle: PlanetSideGUID, from: ActorRef) + /** + * Data structure for passing information about the event to client-local space. + * The fields match the `OrbitalShuttleTimeMsg` packet that is created using this data. + * @see `OrbitalShuttleTimeMsg` + */ + final case class OrbitalShuttleEvent( + u1: HartSequence, + u2: Int, + t1: Long, + t2: Long, + t3: Long, + pairs: List[((PlanetSideGUID, PlanetSideGUID), Int)] + ) + + /** + * Design for the envelop for the message bus + * to relay instructions back to the individual facility amenity portions of this HART system. + * The channel is blank because it does not need special designation. + */ + trait Command extends GenericEventBusMsg { def channel: String = "" } + /** + * Forbid entry through the boartding gantry doors. + */ + case object LockDoors extends Command + /** + * Permit entry through the boartding gantry doors. + */ + case object UnlockDoors extends Command + /** + * The state exists to be turned into, ultimately, a `VehicleStateMessage` packet for the shuttle. + * This state is to be loaded into the `flying` field. + * @see `VehicleStateMessage` + * @param state shuttle state, probably more symbolic of a gvien state than anything else + */ + final case class ShuttleStateUpdate(forChannel: String, state: Int) extends Command + /** + * The shuttle has landed on the pad and will (soon) accept passengers. + */ + final case class ShuttleDocked(forChannel: String) extends Command + /** + * The shuttle has disengaged from the pad, will no longer accept passengers, and may take off soon. + */ + final case class ShuttleFreeFromDock(forChannel: String) extends Command +} diff --git a/src/main/scala/net/psforever/services/hart/HartTimerActions.scala b/src/main/scala/net/psforever/services/hart/HartTimerActions.scala new file mode 100644 index 000000000..5497e64fb --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartTimerActions.scala @@ -0,0 +1,59 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad +import net.psforever.services.local.{LocalAction, LocalServiceMessage} + +object HartTimerActions { + /** + * Update the shuttle's mounted arrangement with the pad, setting the state. + * @param pad the orbital shuttle pad + * @param shuttle the orbital shuttle pad's shuttle + * @param toChannel to whom these messages will be dispatched + */ + def ShuttleDocked(pad: OrbitalShuttlePad, shuttle: Vehicle, toChannel: String): Unit = { + val zone = pad.Zone + if(toChannel.equals(zone.id)) { + shuttle.MountedIn = pad.GUID + } + zone.LocalEvents ! LocalServiceMessage( + toChannel, + LocalAction.ShuttleDock(pad.GUID, shuttle.GUID, 3) + ) + } + + /** + * Update the shuttle's mounted arrangement with the pad, undoing any connection. + * @param pad the orbital shuttle pad + * @param shuttle the orbital shuttle pad's shuttle + * @param toChannel to whom these messages will be dispatched + */ + def ShuttleFreeFromDock(pad: OrbitalShuttlePad, shuttle: Vehicle, toChannel: String): Unit = { + val zone = pad.Zone + if(toChannel.equals(zone.id)) { + shuttle.MountedIn = None + } + zone.LocalEvents ! LocalServiceMessage( + toChannel, + LocalAction.ShuttleUndock(pad.GUID, shuttle.GUID, shuttle.Position, shuttle.Orientation) + ) + } + + /** + * Update the shuttle's flight state. + * @param pad the orbital shuttle pad + * @param shuttle the orbital shuttle pad's shuttle + * @param toChannel to whom these messages will be dispatched + */ + def ShuttleStateUpdate(pad: OrbitalShuttlePad, shuttle: Vehicle, toChannel: String, state: Int): Unit = { + val zone = pad.Zone + if(toChannel.equals(zone.id)) { + shuttle.Flying = state + } + zone.LocalEvents ! LocalServiceMessage( + toChannel, + LocalAction.ShuttleState(shuttle.GUID, shuttle.Position, shuttle.Orientation, state) + ) + } +} \ No newline at end of file diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 8fc581459..cedf3c234 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -2,15 +2,11 @@ package net.psforever.services.local import akka.actor.{Actor, ActorRef, Props} -import akka.pattern.Patterns -import akka.util.Timeout -import net.psforever.actors.zone.{BuildingActor, ZoneActor} import net.psforever.objects.ce.Deployable -import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.objects._ -import net.psforever.packet.game.{PlanetsideAttributeEnum, TriggeredEffect, TriggeredEffectLocation} +import net.psforever.packet.game.{TriggeredEffect, TriggeredEffectLocation} import net.psforever.objects.vital.Vitality import net.psforever.types.{PlanetSideGUID, Vector3} import net.psforever.services.local.support._ @@ -18,12 +14,9 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.{GenericEventBus, RemoverActor, Service} import scala.concurrent.duration._ -import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.vehicles.{Utility, UtilityType} import net.psforever.services.support.SupportActor -import java.util.concurrent.TimeUnit -import scala.concurrent.Await import scala.concurrent.duration.Duration class LocalService(zone: Zone) extends Actor { @@ -91,6 +84,12 @@ class LocalService(zone: Zone) extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.DoorCloses(door_guid)) ) + case LocalAction.DoorSlamsShut(door) => + val door_guid = door.GUID + doorCloser ! SupportActor.HurrySpecific(List(door), zone) + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.DoorCloses(door_guid)) + ) case LocalAction.HackClear(player_guid, target, unk1, unk2) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.SendHackMessageHackCleared(target.GUID, unk1, unk2)) @@ -122,6 +121,14 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.RouterTelepadTransport(passenger_guid, src_guid, dest_guid) ) ) + case LocalAction.SendResponse(pkt) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.SendResponse(pkt) + ) + ) case LocalAction.SetEmpire(object_guid, empire) => LocalEvents.publish( LocalServiceResponse( @@ -130,6 +137,34 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.SetEmpire(object_guid, empire) ) ) + case LocalAction.ShuttleDock(pad, shuttle, slot) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ShuttleDock(pad, shuttle, slot) + ) + ) + case LocalAction.ShuttleUndock(pad, shuttle, pos, orient) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ShuttleUndock(pad, shuttle, pos, orient) + ) + ) + case LocalAction.ShuttleEvent(ev) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.ShuttleEvent(ev)) + ) + case LocalAction.ShuttleState(guid, pos, orient, state) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ShuttleState(guid, pos, orient, state) + ) + ) case LocalAction.ToggleTeleportSystem(player_guid, router, system_plan) => LocalEvents.publish( LocalServiceResponse( @@ -166,7 +201,7 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.TriggerSound(sound, pos, unk, volume) ) ) - case LocalAction.UpdateForceDomeStatus(player_guid, building_guid, activated) => { + case LocalAction.UpdateForceDomeStatus(player_guid, building_guid, activated) => LocalEvents.publish( LocalServiceResponse( s"/$forChannel/Local", @@ -174,7 +209,6 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.UpdateForceDomeStatus(building_guid, activated) ) ) - } case LocalAction.RechargeVehicleWeapon(player_guid, vehicle_guid, weapon_guid) => LocalEvents.publish( LocalServiceResponse( @@ -231,9 +265,9 @@ class LocalService(zone: Zone) extends Actor { if (seats.count(_.isOccupied) > 0) { val wasKickedByDriver = false //TODO yeah, I don't know seats.foreach(seat => { - seat.Occupant match { + seat.occupant match { case Some(tplayer) => - seat.Occupant = None + seat.unmount(tplayer) tplayer.VehicleSeated = None zone.VehicleEvents ! VehicleServiceMessage( zone.id, diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index e4ccd7034..f1a42853a 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -10,7 +10,9 @@ import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.vehicles.Utility import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum +import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{DeployableInfo, DeploymentAction, TriggeredSound} +import net.psforever.services.hart.HartTimer.OrbitalShuttleEvent import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} final case class LocalServiceMessage(forChannel: String, actionMessage: LocalAction.Action) @@ -34,6 +36,7 @@ object LocalAction { final case class Detonate(guid: PlanetSideGUID, obj: PlanetSideGameObject) extends Action final case class DoorOpens(player_guid: PlanetSideGUID, continent: Zone, door: Door) extends Action final case class DoorCloses(player_guid: PlanetSideGUID, door_guid: PlanetSideGUID) extends Action + final case class DoorSlamsShut(door: Door) extends Action final case class HackClear(player_guid: PlanetSideGUID, target: PlanetSideServerObject, unk1: Long, unk2: Long = 8L) extends Action final case class HackTemporarily( @@ -60,7 +63,16 @@ object LocalAction { src_guid: PlanetSideGUID, dest_guid: PlanetSideGUID ) extends Action + final case class SendResponse(pkt: PlanetSideGamePacket) extends Action final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Action + final case class ShuttleDock(pad_guid: PlanetSideGUID, shuttle_guid: PlanetSideGUID, toSlot: Int) extends Action + final case class ShuttleUndock( + pad_guid: PlanetSideGUID, + shuttle_guid: PlanetSideGUID, + pos: Vector3, orient: Vector3 + ) extends Action + final case class ShuttleEvent(ev: OrbitalShuttleEvent) extends Action + final case class ShuttleState(guid: PlanetSideGUID, pos: Vector3, orientation: Vector3, state: Int) extends Action final case class ToggleTeleportSystem( player_guid: PlanetSideGUID, router: Vehicle, diff --git a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala index 1b43cf9aa..f16d06ee0 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala @@ -6,9 +6,11 @@ import net.psforever.objects.ce.Deployable import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.vehicles.Utility import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum +import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.services.GenericEventBusMsg +import net.psforever.services.hart.HartTimer.OrbitalShuttleEvent final case class LocalServiceResponse( channel: String, @@ -43,7 +45,16 @@ object LocalResponse { src_guid: PlanetSideGUID, dest_guid: PlanetSideGUID ) extends Response + final case class SendResponse(pkt: PlanetSideGamePacket) extends Response final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Response + final case class ShuttleDock(pad_guid: PlanetSideGUID, shuttle_guid: PlanetSideGUID, toSlot: Int) extends Response + final case class ShuttleUndock( + pad_guid: PlanetSideGUID, + shuttle_guid: PlanetSideGUID, + pos: Vector3, orient: Vector3 + ) extends Response + final case class ShuttleEvent(ev: OrbitalShuttleEvent) extends Response + final case class ShuttleState(guid: PlanetSideGUID, pos: Vector3, orientation: Vector3, state: Int) extends Response final case class ToggleTeleportSystem( router: Vehicle, systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)] diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala index dab15d0b2..3a89b717e 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala @@ -105,6 +105,14 @@ class VehicleService(zone: Zone) extends Actor { VehicleResponse.KickPassenger(seat_num, kickedByDriver, vehicle_guid) ) ) + case VehicleAction.ObjectDelete(guid) => + VehicleEvents.publish( + VehicleServiceResponse( + s"/$forChannel/Vehicle", + Service.defaultPlayerGUID, + VehicleResponse.ObjectDelete(guid) + ) + ) case VehicleAction.LoadVehicle(player_guid, vehicle, vtype, vguid, vdata) => VehicleEvents.publish( VehicleServiceResponse( diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala index 147d597c0..8f2b5d7be 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala @@ -63,7 +63,7 @@ object VehicleAction { vdata: ConstructorData ) extends Action final case class MountVehicle(player_guid: PlanetSideGUID, object_guid: PlanetSideGUID, seat: Int) extends Action - final case class ObjectDelete(player_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID) extends Action + final case class ObjectDelete(guid: PlanetSideGUID) extends Action final case class Ownership(player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID) extends Action final case class PlanetsideAttribute( player_guid: PlanetSideGUID, diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala index aa9c2ed30..87b724e91 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala @@ -45,11 +45,12 @@ object VehicleResponse { final case class LoadVehicle(vehicle: Vehicle, vtype: Int, vguid: PlanetSideGUID, vdata: ConstructorData) extends Response final case class MountVehicle(object_guid: PlanetSideGUID, seat: Int) extends Response + final case class ObjectDelete(guid: PlanetSideGUID) extends Response final case class Ownership(vehicle_guid: PlanetSideGUID) extends Response final case class PlanetsideAttribute(vehicle_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) extends Response - final case class RevealPlayer(player_guid: PlanetSideGUID) extends Response - final case class SeatPermissions(vehicle_guid: PlanetSideGUID, seat_group: Int, permission: Long) extends Response + final case class RevealPlayer(player_guid: PlanetSideGUID) extends Response + final case class SeatPermissions(vehicle_guid: PlanetSideGUID, seat_group: Int, permission: Long) extends Response final case class StowEquipment( vehicle_guid: PlanetSideGUID, slot: Int, diff --git a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala index 0278da412..cb7ba4e7e 100644 --- a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala +++ b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala @@ -154,8 +154,8 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { target.Seats.values .filter { _.isOccupied } .foreach({ seat => - val tplayer = seat.Occupant.get - seat.Occupant = None + val tplayer = seat.occupant.get + seat.unmount(tplayer) tplayer.VehicleSeated = None if (tplayer.HasGUID) { context.parent ! VehicleServiceMessage( diff --git a/src/main/scala/net/psforever/types/HartSequence.scala b/src/main/scala/net/psforever/types/HartSequence.scala new file mode 100644 index 000000000..01b4fb956 --- /dev/null +++ b/src/main/scala/net/psforever/types/HartSequence.scala @@ -0,0 +1,26 @@ +package net.psforever.types + +import enumeratum.values.{IntEnum, IntEnumEntry} + +sealed abstract class HartSequence(val value: Int) extends IntEnumEntry + +object HartSequence extends IntEnum[HartSequence] { + val values = findValues + + /** no effect, but is used when the shuttle is docked */ + case object State0 extends HartSequence(value = 0) + /** gantries retract, lights on, bay doors open, platform up */ + case object PrepareForDeparture extends HartSequence(value = 1) + /** shuttle takes off, bay doors close, lights off */ + case object TakeOff extends HartSequence(value = 2) + /** lights on, bay doors open, shuttle lands */ + case object Land extends HartSequence(value = 3) + /** platform down, bay doors closed, gantries extend, lights off */ + case object PrepareForBoarding extends HartSequence(value = 4) + /** no effect, but is used when the shuttle is away; + * a substitute for 7 occasionally or used as its supplement, e.g., 2-2, 5-3, 3-4 OR 2-2, 7-3, 5-3, 3-4 + */ + case object State5 extends HartSequence(value = 5) + /** no effect, but is used when the shuttle is away */ + case object State7 extends HartSequence(value = 7) +} diff --git a/src/main/scala/net/psforever/types/Vector3.scala b/src/main/scala/net/psforever/types/Vector3.scala index 76fecca4f..ea821ce06 100644 --- a/src/main/scala/net/psforever/types/Vector3.scala +++ b/src/main/scala/net/psforever/types/Vector3.scala @@ -331,8 +331,8 @@ object Vector3 { val sin = math.sin(ang).toFloat val (x, y, z) = (vec.x, vec.y, vec.z) Vector3( - closeToInsignificance(x * cos - y * sin), - closeToInsignificance(x * sin - y * cos), + closeToInsignificance(x * cos + y * sin), + closeToInsignificance(y * cos - x * sin), z ) } @@ -389,7 +389,7 @@ object Vector3 { * Given a `Vector3` element composed of Euler angles * and a `Vector3` element in the vector direction of "up", * find a standard unit vector that points in the direction of the entity's "up" after rotating by the Euler angles. - * Compass direction rules apply (North is 0 degrees, East is 90 degrees, etc.). + * Compass direction rules apply for the z-axis (North is 0 degrees, East is 90 degrees, etc.). * @see `Vector3.Rx(Float)` * @see `Vector3.Ry(Float)` * @see `Vector3.Rz(Float)` @@ -404,6 +404,6 @@ object Vector3 { I'm sure mathematicians know what's going on here, but I don't the purpose of this comment is to make certain that the future me knows that all this is not a mistake */ - Rz(Rx(Ry(Unit(up), orient.x), orient.y), (orient.z + 180) % 360f) + Rz(Rx(Ry(Unit(up), orient.x), -orient.y), (orient.z + 180) % 360f) } } diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index f85e30ff5..e80a5bbf0 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -136,7 +136,8 @@ case class GameConfig( amenityAutorepairDrainRate: Float, bepRate: Double, cepRate: Double, - newAvatar: NewAvatar + newAvatar: NewAvatar, + hart: HartConfig ) case class NewAvatar( @@ -144,6 +145,11 @@ case class NewAvatar( cr: CommandRank ) +case class HartConfig( + inFlightDuration: Long, + boardingDuration: Long +) + case class DevelopmentConfig( unprivilegedGmCommands: Seq[ChatMessageType], netSim: NetSimConfig diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index e57eed4ff..f1f82a07e 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -14,6 +14,7 @@ import net.psforever.objects.definition.BasicDefinition import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.locks.IFFLock +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.serverobject.pad.{VehicleSpawnPad, VehicleSpawnPadDefinition} import net.psforever.objects.serverobject.painbox.{Painbox, PainboxDefinition} import net.psforever.objects.serverobject.resourcesilo.ResourceSilo @@ -338,6 +339,14 @@ object Zones { owningBuildingGuid = ownerGuid ) + case "gr_door_mb_orb" => + zoneMap + .addLocalObject( + obj.guid, + Door.Constructor(obj.position, GlobalDefinitions.gr_door_mb_orb), + owningBuildingGuid = ownerGuid + ) + case objectType if doorTypes.contains(objectType) => zoneMap .addLocalObject(obj.guid, Door.Constructor(obj.position), owningBuildingGuid = ownerGuid) @@ -395,7 +404,7 @@ object Zones { // presumably the model is rotated differently to the expected orientation // On top of that, some spawn pads also have an additional rotation (vehiclecreationzorientoffset) // when spawning vehicles set in game_objects.adb.lst - this should be handled on the Scala side - val adjustedYaw = closestSpawnPad.yaw - 90; + val adjustedYaw = closestSpawnPad.yaw - 90 zoneMap.addLocalObject( closestSpawnPad.guid, @@ -527,6 +536,15 @@ object Zones { owningBuildingGuid = ownerGuid ) + case "obbasemesh" => + zoneMap + .addLocalObject( + obj.guid, + OrbitalShuttlePad.Constructor(obj.position, GlobalDefinitions.obbasemesh, Vector3.z(obj.yaw)), + owningBuildingGuid = ownerGuid + ) + zoneMap.linkShuttleToBay(obj.guid) + case _ => () } @@ -551,6 +569,8 @@ object Zones { this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.TR) case "home3" => this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.VS) + case zoneid if zoneid.startsWith("c") => + this.map.cavern = true case _ => () } diff --git a/src/test/scala/Vector3Test.scala b/src/test/scala/Vector3Test.scala index 38cb36f96..3d585e82e 100644 --- a/src/test/scala/Vector3Test.scala +++ b/src/test/scala/Vector3Test.scala @@ -205,10 +205,17 @@ class Vector3Test extends Specification { "rotate positive x-axis-vector 90-degrees around the z-axis" in { val A: Vector3 = Vector3(1, 0, 0) A.Rz(0) mustEqual A - A.Rz(90) mustEqual Vector3(0, 1, 0) + A.Rz(90) mustEqual Vector3(0, -1, 0) A.Rz(180) mustEqual Vector3(-1, 0, 0) - A.Rz(270) mustEqual Vector3(0, -1, 0) + A.Rz(270) mustEqual Vector3(0, 1, 0) A.Rz(360) mustEqual A + + val B: Vector3 = Vector3(0, 1, 0) + B.Rz(0) mustEqual B + B.Rz(90) mustEqual Vector3(1, 0, 0) + B.Rz(180) mustEqual Vector3(0, -1, 0) + B.Rz(270) mustEqual Vector3(-1, 0, 0) + B.Rz(360) mustEqual B } "rotate positive y-axis-vector 90-degrees around the x-axis" in { @@ -233,12 +240,12 @@ class Vector3Test extends Specification { val A: Vector3 = Vector3(1, 0, 0) A.Rz(90) .Rx(90) - .Ry(90) mustEqual A + .Ry(-90) mustEqual A } "45-degree rotation" in { val A: Vector3 = Vector3(1, 0, 0) - A.Rz(45) mustEqual Vector3(0.70710677f, 0.70710677f, 0) + A.Rz(45) mustEqual Vector3(0.70710677f, -0.70710677f, 0) } "find a relative up (identity)" in { @@ -291,9 +298,9 @@ class Vector3Test extends Specification { "find a relative up (combined x,z)" in { Vector3.relativeUp(Vector3(0, 0, 90)) mustEqual Vector3(0,0,1) //up - Vector3.relativeUp(Vector3(90, 0, 90)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(90, 0, 90)) mustEqual Vector3(0,1,0) //north Vector3.relativeUp(Vector3(180, 0, 90)) mustEqual Vector3(0,0,-1) //down - Vector3.relativeUp(Vector3(270, 0, 90)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(270, 0, 90)) mustEqual Vector3(0,-1,0) //south Vector3.relativeUp(Vector3(360, 0, 90)) mustEqual Vector3(0,0,1) //up Vector3.relativeUp(Vector3(90, 0, 180)) mustEqual Vector3(1,0,0) //east @@ -301,14 +308,13 @@ class Vector3Test extends Specification { Vector3.relativeUp(Vector3(270, 0, 180)) mustEqual Vector3(-1,0,0) //west Vector3.relativeUp(Vector3(360, 0, 180)) mustEqual Vector3(0,0,1) //up - Vector3.relativeUp(Vector3(90, 0, 270)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(90, 0, 270)) mustEqual Vector3(0,-1,0) //south Vector3.relativeUp(Vector3(180, 0, 270)) mustEqual Vector3(0,0,-1) //down - Vector3.relativeUp(Vector3(270, 0, 270)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(270, 0, 270)) mustEqual Vector3(0,1,0) //north Vector3.relativeUp(Vector3(360, 0, 270)) mustEqual Vector3(0,0,1) //up } "find a relative up (combined x,y)" in { - val south = Vector3(0,-1,0) Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //south Vector3.relativeUp(Vector3(90, 90, 0)) mustEqual Vector3(-1,0,0) //west Vector3.relativeUp(Vector3(180, 90, 0)) mustEqual Vector3(0,1,0) //north diff --git a/src/test/scala/base/FreedContextActorTest.scala b/src/test/scala/base/FreedContextActorTest.scala index 070c07f17..36ae31143 100644 --- a/src/test/scala/base/FreedContextActorTest.scala +++ b/src/test/scala/base/FreedContextActorTest.scala @@ -38,7 +38,7 @@ private class ContextSensitive extends Actor { case _ => context.become(PassThroughBehavior) output = sender() - sender() ! context + sender().tell(context, self) } /** diff --git a/src/test/scala/game/DroppodLaunchRequestMessageTest.scala b/src/test/scala/game/DroppodLaunchRequestMessageTest.scala new file mode 100644 index 000000000..d4de8b53a --- /dev/null +++ b/src/test/scala/game/DroppodLaunchRequestMessageTest.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2021 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 DroppodLaunchRequestMessageTest extends Specification { + val string = hex"53 2405050000e0b24500c06145c0" + + "DroppodLaunchRequestMessage" should { + "decode" in { + PacketCoding.decodePacket(string).require match { + case DroppodLaunchRequestMessage(info, unk) => + info.guid mustEqual PlanetSideGUID(1316) + info.zone_number mustEqual 5 + info.xypos mustEqual Vector3(5724, 3612, 0) + unk mustEqual 3 + case _ => + ko + } + } + + "encode" in { + val msg = DroppodLaunchRequestMessage(PlanetSideGUID(1316), 5, Vector3(5724, 3612, 0)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string + } + } +} diff --git a/src/test/scala/game/DroppodLaunchResponseMessageTest.scala b/src/test/scala/game/DroppodLaunchResponseMessageTest.scala new file mode 100644 index 000000000..f83855109 --- /dev/null +++ b/src/test/scala/game/DroppodLaunchResponseMessageTest.scala @@ -0,0 +1,92 @@ +// Copyright (c) 2021 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 DroppodLaunchResponseMessageTest extends Specification { + val string_1 = hex"55 21506000000000000000000000" + val string_2 = hex"55 9150605000000000000000000640000000a0000000" + + "DroppodLaunchResponseMessage" should { + "decode (1)" in { + PacketCoding.decodePacket(string_1).require match { + case DroppodLaunchResponseMessage(error, info, queue) => + error mustEqual DroppodError.BlockedBySOI + info.guid mustEqual PlanetSideGUID(1557) + info.zone_number mustEqual 0 + info.xypos mustEqual Vector3.Zero + queue.isEmpty mustEqual true + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.decodePacket(string_2).require match { + case DroppodLaunchResponseMessage(error, info, queue) => + error mustEqual DroppodError.ZoneFullWarpQueue + info.guid mustEqual PlanetSideGUID(1557) + info.zone_number mustEqual 5 + info.xypos mustEqual Vector3.Zero + queue.contains(WarpQueuePrompt(100, 10)) mustEqual true + case _ => + ko + } + } + + "encode (1)" in { + val msg = DroppodLaunchResponseMessage(DroppodError.BlockedBySOI, PlanetSideGUID(1557), 0, Vector3.Zero) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_1 + } + + "encode (2)" in { + val msg = DroppodLaunchResponseMessage(PlanetSideGUID(1557), 5, 100, 10) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_2 + } + + "invalid constructors" in { + //invalid + DroppodLaunchResponseMessage( + DroppodError.ZoneFullWarpQueue, + DroppodLaunchInfo(PlanetSideGUID(1557), 5, Vector3.Zero), + None + ) must throwA[AssertionError] + + //acceptable + DroppodLaunchResponseMessage( + DroppodError.BlockedBySOI, + DroppodLaunchInfo(PlanetSideGUID(1557), 5, Vector3.Zero), + Some(WarpQueuePrompt(100, 10)) + ) + ok + } + + "equivalent constructors (1)" in { + DroppodLaunchResponseMessage( + DroppodError.BlockedBySOI, + PlanetSideGUID(1557) + ) mustEqual + DroppodLaunchResponseMessage( + DroppodError.BlockedBySOI, + DroppodLaunchInfo(PlanetSideGUID(1557), 0, Vector3.Zero), + None) + } + + "equivalent constructors (2)" in { + DroppodLaunchResponseMessage(PlanetSideGUID(1557), 5, 100, 10) mustEqual + DroppodLaunchResponseMessage( + DroppodError.ZoneFullWarpQueue, + DroppodLaunchInfo(PlanetSideGUID(1557), 5, Vector3.Zero), + Some(WarpQueuePrompt(100, 10)) + ) + } + } +} diff --git a/src/test/scala/game/OrbitalShuttleTimeMsgTest.scala b/src/test/scala/game/OrbitalShuttleTimeMsgTest.scala new file mode 100644 index 000000000..33657559e --- /dev/null +++ b/src/test/scala/game/OrbitalShuttleTimeMsgTest.scala @@ -0,0 +1,222 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.{HartSequence, PlanetSideGUID} +import scodec.bits._ + +class OrbitalShuttleTimeMsgTest extends Specification { + //these are all from vs sanctuary, near HART A + val string1 = hex"5B 6E3AAE0000200F8000400000000500D9C1051303680414480DA411B0" + val string2 = hex"5B 72A00F8000200F8000400000000500D9C1141303680450480DA41140" + val string3 = hex"5B 62FFFFFFFFA00F8000400000000500D9C1141303680450480DA41140" + val string4 = hex"5B 600000000030750000400000000500D9C1141303680450480DA41140" + val string5 = hex"5B 64F3370180200F8000400000000500D9C1141303680450480DA41140" + val string6 = hex"5B 6953278180200F8000400000000500D9C1061303680464480DA41050" + val string7 = hex"5B 7DCA8D8180200F8000400000000500D9C1141303680450480DA41140" + + "decode (1)" in { + PacketCoding.decodePacket(string1).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.Land + u3 mustEqual 4 + u4 mustEqual 23669 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 5), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 5), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 27) + ) + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.decodePacket(string2).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.PrepareForBoarding + u3 mustEqual 5 + u4 mustEqual 8000 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (3)" in { + PacketCoding.decodePacket(string3).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.State0 + u3 mustEqual 5 + u4 mustEqual 4294967295L + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (4)" in { + PacketCoding.decodePacket(string4).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.State0 + u3 mustEqual 0 + u4 mustEqual 0 + u5 mustEqual 60000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (5)" in { + PacketCoding.decodePacket(string5).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.PrepareForDeparture + u3 mustEqual 1 + u4 mustEqual 224998 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (6)" in { + PacketCoding.decodePacket(string6).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.TakeOff + u3 mustEqual 2 + u4 mustEqual 216998 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 6), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 25), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 5) + ) + case _ => + ko + } + } + + "decode (7)" in { + PacketCoding.decodePacket(string7).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.State7 + u3 mustEqual 3 + u4 mustEqual 203669 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "encode (1)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.Land, 4, 23669, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 5), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 5), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 27) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string1 + } + + "encode (2)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.PrepareForBoarding, 5, 8000, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string2 + } + + "encode (3)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.State0, 5, 4294967295L, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string3 + } + + "encode (4)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.State0, 0, 0, 60000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string4 + } + + "encode (5)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.PrepareForDeparture, 1, 224998, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string5 + } + + "encode (6)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.TakeOff, 2, 216998, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 6), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 25), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 5) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string6 + } + + "encode (7)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.State7, 3, 203669, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string7 + } +} diff --git a/src/test/scala/objects/ConverterTest.scala b/src/test/scala/objects/ConverterTest.scala index 3f26716c5..f1c56c177 100644 --- a/src/test/scala/objects/ConverterTest.scala +++ b/src/test/scala/objects/ConverterTest.scala @@ -8,6 +8,7 @@ import net.psforever.objects.definition._ import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.locker.{LockerContainer, LockerEquipment} +import net.psforever.objects.serverobject.mount.{MountInfo, SeatDefinition} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.UtilityType @@ -872,10 +873,10 @@ class ConverterTest extends Specification { val fury_def = VehicleDefinition(ObjectClass.fury) fury_def.Seats += 0 -> new SeatDefinition() - fury_def.Seats(0).Bailable = true - fury_def.Seats(0).ControlledWeapon = Some(1) - fury_def.MountPoints += 0 -> 0 - fury_def.MountPoints += 2 -> 0 + fury_def.Seats(0).bailable = true + fury_def.controlledWeapons += 0 -> 1 + fury_def.MountPoints += 0 -> MountInfo(0) + fury_def.MountPoints += 2 -> MountInfo(0) fury_def.Weapons += 1 -> fury_weapon_systema_def fury_def.TrunkSize = InventoryTile(11, 11) fury_def.TrunkOffset = 30 diff --git a/src/test/scala/objects/DamageableTest.scala b/src/test/scala/objects/DamageableTest.scala index d3c9e0628..e5c218f1d 100644 --- a/src/test/scala/objects/DamageableTest.scala +++ b/src/test/scala/objects/DamageableTest.scala @@ -631,8 +631,8 @@ class DamageableMountableDamageTest extends ActorTest { Vector3(1, 0, 0) ) val applyDamageTo = resolved.calculate() - mech.Seats(0).Occupant = player2 //seat the player - player2.VehicleSeated = Some(mech.GUID) //seat the player + mech.Seats(0).mount(player2) //mount the player + player2.VehicleSeated = Some(mech.GUID) //mount the player expectNoMessage(200 milliseconds) //we're not testing that the math is correct @@ -728,8 +728,8 @@ class DamageableMountableDestroyTest extends ActorTest { Vector3(1, 0, 0) ) val applyDamageTo = resolved.calculate() - mech.Seats(0).Occupant = player2 //seat the player - player2.VehicleSeated = Some(mech.GUID) //seat the player + mech.Seats(0).mount(player2) //mount the player + player2.VehicleSeated = Some(mech.GUID) //mount the player expectNoMessage(200 milliseconds) //we're not testing that the math is correct @@ -797,7 +797,7 @@ class DamageableWeaponTurretDamageTest extends ActorTest { guid.register(turret, 2) guid.register(player1, 3) guid.register(player2, 4) - turret.Seats(0).Occupant = player2 + turret.Seats(0).mount(player2) player2.VehicleSeated = turret.GUID val weapon = Tool(GlobalDefinitions.suppressor) @@ -898,7 +898,7 @@ class DamageableWeaponTurretJammerTest extends ActorTest { guid.register(player2, 4) guid.register(turretWeapon, 5) guid.register(turretWeapon.AmmoSlot.Box, 6) - turret.Seats(0).Occupant = player2 + turret.Seats(0).mount(player2) player2.VehicleSeated = turret.GUID val weapon = Tool(GlobalDefinitions.jammer_grenade) @@ -999,7 +999,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { guid.register(player2, 4) guid.register(turretWeapon, 5) guid.register(turretWeapon.AmmoSlot.Box, 6) - turret.Seats(0).Occupant = player2 + turret.Seats(0).mount(player2) player2.VehicleSeated = turret.GUID building.Position = Vector3(1, 0, 0) building.Zone = zone @@ -1060,7 +1060,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { assert(!turret.Destroyed) turret.Actor ! Vitality.Damage(applyDamageToA) //also test destruction while jammered - vehicleProbe.receiveN(2, 500 milliseconds) //flush jammered messages (see above) + vehicleProbe.receiveN(2, 1000 milliseconds) //flush jammered messages (see above) assert(turret.Health > turret.Definition.DamageDestroysAt) assert(turret.Jammed) assert(!turret.Destroyed) @@ -1148,7 +1148,7 @@ class DamageableVehicleDamageTest extends ActorTest { guid.register(player1, 2) guid.register(player2, 3) atv.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID val weapon = Tool(GlobalDefinitions.suppressor) @@ -1273,12 +1273,12 @@ class DamageableVehicleDamageMountedTest extends ActorTest { //the lodestar control actor needs to load after the utilities have guid's assigned lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") lodestar.Zone = zone - lodestar.Seats(0).Occupant = player2 + lodestar.Seats(0).mount(player2) player2.VehicleSeated = lodestar.GUID atv.Zone = zone - atv.Seats(0).Occupant = player3 + atv.Seats(0).mount(player3) player3.VehicleSeated = atv.GUID - lodestar.CargoHolds(1).Occupant = atv + lodestar.CargoHolds(1).mount(atv) atv.MountedIn = lodestar.GUID val weapon = Tool(GlobalDefinitions.phoenix) //decimator @@ -1420,11 +1420,11 @@ class DamageableVehicleJammeringMountedTest extends ActorTest { lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") atv.Zone = zone lodestar.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID - lodestar.Seats(0).Occupant = player3 + lodestar.Seats(0).mount(player3) player3.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = atv + lodestar.CargoHolds(1).mount(atv) atv.MountedIn = lodestar.GUID val vehicleSource = SourceEntry(lodestar) @@ -1516,7 +1516,7 @@ class DamageableVehicleDestroyTest extends ActorTest { guid.register(atvWeapon, 4) guid.register(atvWeapon.AmmoSlot.Box, 5) atv.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID val weapon = Tool(GlobalDefinitions.suppressor) @@ -1642,11 +1642,11 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") atv.Zone = zone lodestar.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID - lodestar.Seats(0).Occupant = player3 + lodestar.Seats(0).mount(player3) player3.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = atv + lodestar.CargoHolds(1).mount(atv) atv.MountedIn = lodestar.GUID val vehicleSource = SourceEntry(lodestar) diff --git a/src/test/scala/objects/DeployableTest.scala b/src/test/scala/objects/DeployableTest.scala index 1fa1db7b5..3f73d8b1f 100644 --- a/src/test/scala/objects/DeployableTest.scala +++ b/src/test/scala/objects/DeployableTest.scala @@ -8,7 +8,7 @@ import net.psforever.objects.ballistics._ import net.psforever.objects.ce.DeployedItem import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.mount.{MountInfo, Mountable} import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.{TurretDeployable, _} @@ -156,8 +156,11 @@ class TurretDeployableTest extends Specification { } "may have mount point" in { - new TurretDeployable(GlobalDefinitions.spitfire_turret).MountPoints mustEqual Map() - new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs).MountPoints mustEqual Map(1 -> 0, 2 -> 0) + new TurretDeployable(GlobalDefinitions.spitfire_turret).MountPoints.isEmpty mustEqual true + + val pmt = new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs) + pmt.MountPoints.get(1).contains(MountInfo(0, Vector3(0, 0, 0))) mustEqual true + pmt.MountPoints.get(2).contains(MountInfo(0, Vector3(0, 0, 0))) mustEqual true } } } @@ -669,15 +672,15 @@ class TurretControlMountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player1, 0) + obj.Actor ! Mountable.TryMount(player1, 1) val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player1) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player1)) + assert(obj.Seats(0).occupant.contains(player1)) } } } @@ -689,18 +692,18 @@ class TurretControlBlockMountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player1, 0) + obj.Actor ! Mountable.TryMount(player1, 1) val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player1) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player1)) + assert(obj.Seats(0).occupant.contains(player1)) val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player2, 0) + obj.Actor ! Mountable.TryMount(player2, 1) val reply2a = receiveOne(200 milliseconds) assert(reply2a.isInstanceOf[Mountable.MountMessages]) val reply2b = reply2a.asInstanceOf[Mountable.MountMessages] @@ -717,15 +720,15 @@ class TurretControlBlockBetrayalMountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) val player = Player(Avatar(0, "test", PlanetSideEmpire.VS, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player) assert(reply1b.response.isInstanceOf[Mountable.CanNotMount]) - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) } } } @@ -737,15 +740,15 @@ class TurretControlDismountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player)) + assert(obj.Seats(0).occupant.contains(player)) obj.Actor ! Mountable.TryDismount(player, 0) val reply2a = receiveOne(200 milliseconds) @@ -753,7 +756,7 @@ class TurretControlDismountTest extends ActorTest { val reply2b = reply2a.asInstanceOf[Mountable.MountMessages] assert(reply2b.player == player) assert(reply2b.response.isInstanceOf[Mountable.CanDismount]) - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) } } } @@ -762,21 +765,25 @@ class TurretControlBetrayalMountTest extends ActorTest { "TurretControl" should { "allow all allegiances" in { val obj = new TurretDeployable( - new TurretDeployableDefinition(685) { FactionLocked = false } //required (defaults to true) + new TurretDeployableDefinition(685) { + MountPoints += 1 -> MountInfo(0, Vector3.Zero) + FactionLocked = false + } //required (defaults to true) ) { GUID = PlanetSideGUID(1) } obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + val probe = new TestProbe(system) - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) val player = Player(Avatar(0, "test", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) assert(player.Faction != obj.Faction) - obj.Actor ! Mountable.TryMount(player, 0) - val reply1a = receiveOne(200 milliseconds) + obj.Actor.tell(Mountable.TryMount(player, 1), probe.ref) + val reply1a = probe.receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player)) + assert(obj.Seats(0).occupant.contains(player)) } } } diff --git a/src/test/scala/objects/EnvironmentTest.scala b/src/test/scala/objects/EnvironmentTest.scala index 51613ef1e..ed848f21c 100644 --- a/src/test/scala/objects/EnvironmentTest.scala +++ b/src/test/scala/objects/EnvironmentTest.scala @@ -1,13 +1,14 @@ // Copyright (c) 2020 PSForever package objects +import net.psforever.objects.avatar.Avatar import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle} import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.terminals.{Terminal, TerminalDefinition} import net.psforever.objects.vital.Vitality import net.psforever.packet.game.objectcreate.ObjectClass -import net.psforever.types.Vector3 +import net.psforever.types._ import org.specs2.mutable.Specification class EnvironmentCollisionTest extends Specification { @@ -203,6 +204,26 @@ class EnvironmentAttributeTest extends Specification { EnvironmentAttribute.Death.canInteractWith(obj) mustEqual false } } + + "GantryDenialField" should { + "interact with players" in { + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + obj.Spawn() + EnvironmentAttribute.GantryDenialField.canInteractWith(obj) mustEqual true + } + + "not interact with dead players" in { + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + obj.isAlive mustEqual false + EnvironmentAttribute.GantryDenialField.canInteractWith(obj) mustEqual false + } + + "not interact with an object that is not a player" in { + val obj = Tool(GlobalDefinitions.suppressor) + obj.isInstanceOf[Vitality] mustEqual false + EnvironmentAttribute.GantryDenialField.canInteractWith(obj) mustEqual false + } + } } class SeaLevelTest extends Specification { @@ -281,6 +302,17 @@ class PoolTest extends Specification { } } +class GantryDenialField extends Specification { + val square = DeepSquare(0, 1, 10, 10, 1) + + "GantryDenialField" should { + "always has the environmental attribute of 'GantryDenialField'" in { + val obj = GantryDenialField(PlanetSideGUID(0), 0, square) + obj.attribute mustEqual EnvironmentAttribute.GantryDenialField + } + } +} + class PieceOfEnvironmentTest extends Specification { "PieceOfEnvironment" should { import PieceOfEnvironment.testStepIntoInteraction diff --git a/src/test/scala/objects/FacilityTurretTest.scala b/src/test/scala/objects/FacilityTurretTest.scala index 453303909..99d1f8b97 100644 --- a/src/test/scala/objects/FacilityTurretTest.scala +++ b/src/test/scala/objects/FacilityTurretTest.scala @@ -10,7 +10,7 @@ import net.psforever.objects.definition.ToolDefinition import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.mount.{MountInfo, Mountable} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.turret._ import net.psforever.objects.zones.{Zone, ZoneMap} @@ -31,7 +31,7 @@ class FacilityTurretTest extends Specification { obj.ReserveAmmunition mustEqual false obj.FactionLocked mustEqual true obj.MaxHealth mustEqual 0 - obj.MountPoints mustEqual mutable.HashMap.empty[Int, Int] + obj.MountPoints.isEmpty mustEqual true } "construct" in { @@ -44,9 +44,8 @@ class FacilityTurretTest extends Specification { ko } obj.Seats.size mustEqual 1 - obj.Seats(0).ControlledWeapon.contains(1) mustEqual true obj.MountPoints.size mustEqual 1 - obj.MountPoints(1) mustEqual 0 + obj.MountPoints.get(1).contains(MountInfo(0, Vector3.Zero)) mustEqual true obj.Health mustEqual 3600 obj.Upgrade mustEqual TurretUpgrade.None obj.Health = 360 @@ -110,12 +109,12 @@ class FacilityTurretControl2Test extends ActorTest { bldg.Faction = PlanetSideEmpire.TR "FacilityTurretControl" should { - "seat on faction affiliation when FactionLock is true" in { + "mount on faction affiliation when FactionLock is true" in { assert(player.Faction == PlanetSideEmpire.TR) assert(obj.Faction == PlanetSideEmpire.TR) assert(obj.Definition.FactionLocked) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply = receiveOne(300 milliseconds) reply match { case msg: Mountable.MountMessages => @@ -141,7 +140,7 @@ class FacilityTurretControl3Test extends ActorTest { assert(obj.Faction == PlanetSideEmpire.NEUTRAL) assert(obj.Definition.FactionLocked) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply = receiveOne(300 milliseconds) reply match { case msg: Mountable.MountMessages => @@ -155,9 +154,7 @@ class FacilityTurretControl3Test extends ActorTest { class FacilityTurretControl4Test extends ActorTest { val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - val objDef = new FacilityTurretDefinition(480) - objDef.FactionLocked = false - val obj = FacilityTurret(objDef) + val obj = FacilityTurret(GlobalDefinitions.vanu_sentry_turret) obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") val bldg = Building("Building", guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building) @@ -169,7 +166,7 @@ class FacilityTurretControl4Test extends ActorTest { assert(obj.Faction == PlanetSideEmpire.NEUTRAL) assert(!obj.Definition.FactionLocked) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply = receiveOne(300 milliseconds) reply match { case msg: Mountable.MountMessages => diff --git a/src/test/scala/objects/MountableTest.scala b/src/test/scala/objects/MountableTest.scala index 5b7221617..e761d4d35 100644 --- a/src/test/scala/objects/MountableTest.scala +++ b/src/test/scala/objects/MountableTest.scala @@ -5,10 +5,9 @@ import akka.actor.{Actor, ActorRef, Props} import base.ActorTest import net.psforever.objects.Player import net.psforever.objects.avatar.Avatar -import net.psforever.objects.definition.{ObjectDefinition, SeatDefinition} -import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.mount._ import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.vehicles.Seat import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, PlanetSideGUID} import scala.concurrent.duration.Duration @@ -39,7 +38,7 @@ class MountableControl2Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanMount] assert(reply3.obj == obj) - assert(reply3.seat_num == 0) + assert(reply3.seat_number == 0) } } } @@ -62,35 +61,25 @@ class MountableControl3Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanNotMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanNotMount] assert(reply3.obj == obj) - assert(reply3.seat_num == 0) + assert(reply3.mount_point == 0) } } } object MountableTest { class MountableTestObject extends PlanetSideServerObject with Mountable { - private val seats: Map[Int, Seat] = Map(0 -> new Seat(new SeatDefinition())) - def Seats: Map[Int, Seat] = seats - def Seat(seatNum: Int): Option[Seat] = seats.get(seatNum) - def MountPoints: Map[Int, Int] = Map(1 -> 0) - def GetSeatFromMountPoint(mount: Int): Option[Int] = MountPoints.get(mount) - def PassengerInSeat(user: Player): Option[Int] = { - if (seats(0).Occupant.contains(user)) { - Some(0) - } else { - None - } - } + seats += 0 -> new Seat(new SeatDefinition()) GUID = PlanetSideGUID(1) //eh whatever def Faction = PlanetSideEmpire.TR - def Definition: ObjectDefinition = null + def Definition = new ObjectDefinition(1) with MountableDefinition { + MountPoints += 0 -> MountInfo(0) + } } class MountableTestControl(obj: PlanetSideServerObject with Mountable) extends Actor - with MountableBehavior.Mount - with MountableBehavior.Dismount { + with MountableBehavior { override def MountableObject = obj def receive: Receive = mountBehavior.orElse(dismountBehavior) diff --git a/src/test/scala/objects/OrbitalShuttlePadTest.scala b/src/test/scala/objects/OrbitalShuttlePadTest.scala new file mode 100644 index 000000000..cce0ba303 --- /dev/null +++ b/src/test/scala/objects/OrbitalShuttlePadTest.scala @@ -0,0 +1,100 @@ +// Copyright (c) 2021 PSForever +package objects + +import akka.actor.{ActorRef, Props} +import akka.routing.RandomPool +import akka.testkit.TestProbe +import base.FreedContextActorTest +import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.guid.actor.UniqueNumberSystem +import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.objects.guid.{NumberPoolHub, TaskResolver} +import net.psforever.objects.guid.source.MaxNumberSource +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.shuttle.{OrbitalShuttle, OrbitalShuttlePad, OrbitalShuttlePadControl, ShuttleAmenity} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.zones.{Zone, ZoneMap, ZoneVehicleActor} +import net.psforever.services.{InterstellarClusterService, Service, ServiceManager} +import net.psforever.services.galaxy.GalaxyService +import net.psforever.services.hart.HartService +import net.psforever.types.PlanetSideEmpire + +import scala.collection.concurrent.TrieMap +import scala.collection.mutable.ListBuffer +import scala.concurrent.duration._ + +class OrbitalShuttlePadControltest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + val services = ServiceManager.boot(system) + services ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + services ! ServiceManager.Register(Props[HartService](), "hart") + expectNoMessage(1000 milliseconds) + var buildingMap = new TrieMap[Int, Building]() + val vehicles = ListBuffer[Vehicle]() + val guid = new NumberPoolHub(new MaxNumberSource(max = 15)) + guid.AddPool("dynamic", (11 to 15).toList) + val catchall = new TestProbe(system).ref + val resolver = context.actorOf(RandomPool(1).props(Props[TaskResolver]()), s"test-taskResolver") + val uns = context.actorOf( + RandomPool(1).props( + Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(this.guid)) + ), + s"test-uns" + ) + val zone = new Zone("test", new ZoneMap("test-map"), 0) { + val transport: ActorRef = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"zone-test-vehicles") + + override def SetupNumberPools() = {} + GUID(guid) + override def GUID = { uns } + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + override def Transport = { transport } + override def Vehicles = { vehicles.toList } + override def Buildings = { buildingMap.toMap } + override def tasks = { resolver } + } + val building = new Building( + name = "test-orbital-building-tr", + building_guid = 1, + map_id = 0, + zone, + StructureType.Building, + GlobalDefinitions.orbital_building_tr + ) + building.Faction = PlanetSideEmpire.TR + buildingMap += 1 -> building + building.Actor = context.spawn(BuildingActor(zone, building), "test-orbital-building-tr-control").toClassic + building.Invalidate() + guid.register(building, number = 1) + + (3 to 10).foreach { index => + val door = Door(GlobalDefinitions.gr_door_mb_orb) + building.Amenities = door + door.Actor = catchall + guid.register(door, index) + } + + val pad = new OrbitalShuttlePad(GlobalDefinitions.obbasemesh) + guid.register(pad, number = 2) + pad.Actor = system.actorOf(Props(classOf[OrbitalShuttlePadControl], pad), "test-shuttle-pad") + building.Amenities = pad + + "OrbitalShuttlePad" should { + "startup and create the shuttle" in { + assert(building.Amenities.size == 9) + assert(vehicles.isEmpty) + pad.Actor ! Service.Startup() + expectNoMessage(max = 5 seconds) + assert(building.Amenities.size == 10) + assert(vehicles.size == 1) + assert(building.Amenities(9).isInstanceOf[ShuttleAmenity]) //the shuttle is an amenity of the building now + assert(vehicles.head.isInstanceOf[OrbitalShuttle]) //here is the shuttle + } + } +} + +object OrbitalShuttlePadTest { /* intentionally blank */ } diff --git a/src/test/scala/objects/OrbitalShuttleTest.scala b/src/test/scala/objects/OrbitalShuttleTest.scala new file mode 100644 index 000000000..aebce4f2b --- /dev/null +++ b/src/test/scala/objects/OrbitalShuttleTest.scala @@ -0,0 +1,274 @@ +// Copyright (c) 2021 PSForever +package objects + +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.avatar.Avatar +import net.psforever.objects.serverobject.shuttle.OrbitalShuttle +import net.psforever.objects.vehicles.AccessPermissionGroup +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import org.specs2.mutable.Specification + +class OrbitalShuttleTest extends Specification { + val testAvatar1 = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + val testAvatar2 = Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + val testAvatar3 = Avatar(2, "TestCharacter3", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + + "OrbitalShuttle" should { + "construct (proper definition)" in { + new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + ok + } + + "construct (any type of vehicle)" in { + new OrbitalShuttle(GlobalDefinitions.fury) + ok + } + + "only use known mount points" in { + val fury = new OrbitalShuttle(GlobalDefinitions.fury) + fury.MountPoints.get(0).isEmpty mustEqual true + fury.MountPoints.get(1).nonEmpty mustEqual true + fury.MountPoints.get(2).nonEmpty mustEqual true + fury.MountPoints.get(3).isEmpty mustEqual true + + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + shuttle.MountPoints.get(0).isEmpty mustEqual true + shuttle.MountPoints.get(1).nonEmpty mustEqual true + shuttle.MountPoints.get(2).nonEmpty mustEqual true + shuttle.MountPoints.get(3).nonEmpty mustEqual true + shuttle.MountPoints.get(4).nonEmpty mustEqual true + shuttle.MountPoints.get(5).nonEmpty mustEqual true + shuttle.MountPoints.get(6).nonEmpty mustEqual true + shuttle.MountPoints.get(7).nonEmpty mustEqual true + shuttle.MountPoints.get(8).nonEmpty mustEqual true + shuttle.MountPoints.get(9).isEmpty mustEqual true + } + + "will only discover unoccupied seats" in { + val fury1 = new OrbitalShuttle(GlobalDefinitions.fury) + val player1 = Player(testAvatar1) + fury1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + case _ => ko + } + fury1.GetSeatFromMountPoint(mountPoint = 1).isEmpty mustEqual true //seat is occupied + + //comparison with normal Vehicle + val fury2 = new Vehicle(GlobalDefinitions.fury) + val player2 = Player(testAvatar2) + fury2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + case _ => ko + } + fury2.GetSeatFromMountPoint(mountPoint = 1).contains(0) mustEqual true //even though seat is occupied + } + + "have a fixed number of normal seats (using normal definition)" in { + val fury1 = new OrbitalShuttle(GlobalDefinitions.fury) + fury1.Seats.size mustEqual 1 + fury1.MountPoints.size mustEqual 2 + val player1 = Player(testAvatar1) + fury1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + case _ => ko + } + fury1.Seats.size mustEqual 1 + fury1.MountPoints.size mustEqual 2 + val player2 = Player(testAvatar2) + fury1.GetSeatFromMountPoint(mountPoint = 1).isEmpty mustEqual true + fury1.Seats.size mustEqual 1 + fury1.MountPoints.size mustEqual 2 + + //congruent with normal Vehicle + val fury2 = new Vehicle(GlobalDefinitions.fury) + fury2.Seats.size mustEqual 1 + fury2.MountPoints.size mustEqual 2 + val player3 = Player(testAvatar3) + fury2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + case _ => ko + } + fury2.Seats.size mustEqual 1 + fury2.MountPoints.size mustEqual 2 + fury2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual false + case _ => ko + } + case _ => ko + } + fury2.Seats.size mustEqual 1 + fury2.MountPoints.size mustEqual 2 + } + + "create seats as needed (with appropriate definition)" in { + GlobalDefinitions.fury + .Seats(0).occupancy == 1 mustEqual true + GlobalDefinitions.orbital_shuttle + .Seats(0).occupancy > 1 mustEqual true + + val shuttle1 = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + shuttle1.Seats.size mustEqual 1 + shuttle1.MountPoints.size mustEqual 8 + val player1 = Player(testAvatar1) + shuttle1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + case _ => ko + } + shuttle1.Seats.size mustEqual 1 + shuttle1.MountPoints.size mustEqual 8 + val player2 = Player(testAvatar2) + shuttle1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + case _ => ko + } + shuttle1.Seats.size mustEqual 2 + shuttle1.MountPoints.size mustEqual 8 + + //comparison with normal Vehicle + val shuttle2 = new Vehicle(GlobalDefinitions.orbital_shuttle) + shuttle2.Seats.size mustEqual 1 + shuttle2.MountPoints.size mustEqual 8 + val player3 = Player(testAvatar3) + shuttle2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + case _ => ko + } + shuttle2.Seats.size mustEqual 1 + shuttle2.MountPoints.size mustEqual 8 + shuttle2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual false + case _ => ko + } + case _ => ko + } + shuttle2.Seats.size mustEqual 1 + shuttle2.MountPoints.size mustEqual 8 + } + + "not create new seats out of order" in { + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + val player1 = Player(testAvatar1) + shuttle.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle.Seat(seatNumber = 2).isEmpty mustEqual true + } + + "recognize proper seating arrangements" in { + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + val player1 = Player(testAvatar1) + shuttle.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle.Seat(seatNumber = 1) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + val player3 = Player(testAvatar3) + shuttle.Seat(seatNumber = 2) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + shuttle.PassengerInSeat(player1).contains(0) mustEqual true + shuttle.PassengerInSeat(player2).contains(1) mustEqual true + shuttle.PassengerInSeat(player3).contains(2) mustEqual true + } + + "retain created seats after dismount" in { + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + val player1 = Player(testAvatar1) + shuttle.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle.Seat(seatNumber = 1) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + val player3 = Player(testAvatar3) + shuttle.Seat(seatNumber = 2) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + + shuttle.Seats(0).isOccupied mustEqual true + shuttle.Seats(1).isOccupied mustEqual true + shuttle.Seats(2).isOccupied mustEqual true + shuttle.Seats.size mustEqual 3 + //IMPORTANT TO NOTE + shuttle.GetSeatFromMountPoint(mountPoint = 1).contains(3) mustEqual true //new seat + + shuttle.Seat(seatNumber = 1) match { + case Some(seat) => seat.unmount(player2).isEmpty mustEqual true + case _ => ko + } + shuttle.Seats(0).isOccupied mustEqual true + shuttle.Seats(1).isOccupied mustEqual false + shuttle.Seats(2).isOccupied mustEqual true + shuttle.Seats.size mustEqual 3 + //IMPORTANT TO NOTE + shuttle.GetSeatFromMountPoint(mountPoint = 1).contains(1) mustEqual true //reuse newly unoccupied seat + } + + "consider all seats as passenger seats" in { + val fury1 = Vehicle(GlobalDefinitions.fury) + fury1.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Driver) + fury1.SeatPermissionGroup(seatNumber = 1).isEmpty mustEqual true + + val fury2 = Vehicle(GlobalDefinitions.orbital_shuttle) + fury2.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Driver) + fury2.SeatPermissionGroup(seatNumber = 1).isEmpty mustEqual true + + val shuttle1 = new OrbitalShuttle(GlobalDefinitions.fury) + shuttle1.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Passenger) + shuttle1.SeatPermissionGroup(seatNumber = 1).isEmpty mustEqual true + + val shuttle2 = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + shuttle2.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Passenger) + shuttle2.SeatPermissionGroup(seatNumber = 1).contains(AccessPermissionGroup.Passenger) //seat does not exist yet + shuttle2.SeatPermissionGroup(seatNumber = 2).isEmpty mustEqual true + val player1 = Player(testAvatar1) + shuttle2.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle2.Seat(seatNumber = 1) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + shuttle2.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Passenger) + shuttle2.SeatPermissionGroup(seatNumber = 1).contains(AccessPermissionGroup.Passenger) + shuttle2.SeatPermissionGroup(seatNumber = 2).contains(AccessPermissionGroup.Passenger) //seat does not exist yet + shuttle2.SeatPermissionGroup(seatNumber = 3).isEmpty mustEqual true + } + } +} diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index 4bde843f7..7af1b712b 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -660,7 +660,7 @@ class PlayerControlDeathSeatedTest extends ActorTest { "handle death when seated (in something)" in { player2.Health = player2.Definition.DamageDestroysAt + 1 //initial state manip player2.VehicleSeated = vehicle.GUID //initial state manip, anything - vehicle.Seats(0).Occupant = player2 + vehicle.Seats(0).mount(player2) player2.Armor = 0 //initial state manip assert(player2.Health > player2.Definition.DamageDestroysAt) assert(player2.isAlive) diff --git a/src/test/scala/objects/PlayerTest.scala b/src/test/scala/objects/PlayerTest.scala index d8f91da8b..a4bd0c13d 100644 --- a/src/test/scala/objects/PlayerTest.scala +++ b/src/test/scala/objects/PlayerTest.scala @@ -336,7 +336,7 @@ class PlayerTest extends Specification { } //free hand } - "seat in a vehicle" in { + "mount in a vehicle" in { val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.VehicleSeated.isEmpty mustEqual true obj.VehicleSeated = PlanetSideGUID(65) diff --git a/src/test/scala/objects/ResourceSiloTest.scala b/src/test/scala/objects/ResourceSiloTest.scala index f0cd6c830..a6f5e06ce 100644 --- a/src/test/scala/objects/ResourceSiloTest.scala +++ b/src/test/scala/objects/ResourceSiloTest.scala @@ -3,8 +3,8 @@ package objects import akka.actor.{Actor, Props} import akka.testkit.TestProbe -import base.ActorTest -import net.psforever.actors.zone.{BuildingActor, ZoneActor} +import base.{ActorTest, FreedContextActorTest} +import net.psforever.actors.zone.BuildingActor import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.CommonMessages @@ -17,9 +17,11 @@ import net.psforever.packet.game.UseItemMessage import net.psforever.types._ import org.specs2.mutable.Specification import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import akka.actor.typed.scaladsl.adapter._ import net.psforever.objects.avatar.Avatar +import net.psforever.services.{InterstellarClusterService, ServiceManager} +import net.psforever.services.galaxy.GalaxyService +import scala.collection.concurrent.TrieMap import scala.concurrent.duration._ class ResourceSiloTest extends Specification { @@ -145,51 +147,60 @@ class ResourceSiloControlStartupMessageSomeTest extends ActorTest { } } -class ResourceSiloControlUseTest extends ActorTest { - val guid = new NumberPoolHub(new MaxNumberSource(10)) - val map = new ZoneMap("test") - val zone = new Zone("test", map, 0) { +class ResourceSiloControlUseTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + var buildingMap = new TrieMap[Int, Building]() + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val ant = Vehicle(GlobalDefinitions.ant) + val silo = new ResourceSilo() + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test-map"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + override def Vehicles = List(ant) + override def Buildings = { buildingMap.toMap } } - zone.actor = system.spawnAnonymous(ZoneActor(zone)) val building = new Building( - "Building", - building_guid = 0, + name = "integ-fac-test-building", + building_guid = 6, map_id = 0, zone, - StructureType.Building, - GlobalDefinitions.building - ) //guid=1 - building.Actor = TestProbe("building-actor").ref + StructureType.Facility, + GlobalDefinitions.cryo_facility + ) + buildingMap += 6 -> building + building.Actor = context.spawn(BuildingActor(zone, building), "integ-fac-test-building-control").toClassic + building.Invalidate() - val obj = ResourceSilo() //guid=2 - obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") - obj.Owner = building - obj.Actor ! "startup" + guid.register(player, number = 1) + guid.register(ant, number = 2) + guid.register(silo, number = 5) + guid.register(building, number = 6) - val player = Player( - new Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) - ) //guid=3 - val vehicle = Vehicle(GlobalDefinitions.ant) //guid=4 - val probe = new TestProbe(system) - - guid.register(building, 1) - guid.register(obj, 2) - guid.register(player, 3) - guid.register(vehicle, 4) - expectNoMessage(200 milliseconds) - zone.Transport ! Zone.Vehicle.Spawn(vehicle) - vehicle.Seats(0).Occupant = player - player.VehicleSeated = vehicle.GUID - expectNoMessage(200 milliseconds) - system.stop(vehicle.Actor) - vehicle.Actor = probe.ref + val maxNtuCap = ant.Definition.MaxNtuCapacitor + player.Spawn() + ant.NtuCapacitor = maxNtuCap + val probe = new TestProbe(system) + ant.Actor = probe.ref + ant.Zone = zone + ant.Seats(0).mount(player) + ant.DeploymentState = DriveState.Deployed + building.Amenities = silo + silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") + silo.Actor ! "startup" "Resource silo" should { "respond when being used" in { expectNoMessage(1 seconds) - obj.Actor ! CommonMessages.Use(ResourceSiloTest.player) + silo.Actor ! CommonMessages.Use(ResourceSiloTest.player) val reply = probe.receiveOne(2000 milliseconds) assert(reply match { diff --git a/src/test/scala/objects/ServerObjectBuilderTest.scala b/src/test/scala/objects/ServerObjectBuilderTest.scala index caefd9f9f..448633d0d 100644 --- a/src/test/scala/objects/ServerObjectBuilderTest.scala +++ b/src/test/scala/objects/ServerObjectBuilderTest.scala @@ -226,6 +226,21 @@ class FacilityTurretObjectBuilderTest extends FreedContextActorTest { } } +class OrbitalShuttlePadObjectBuilderTest extends FreedContextActorTest { + import net.psforever.objects.GlobalDefinitions.obbasemesh + import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad + "OrbitalShuttlePadObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val obj = ServerObjectBuilder(1, OrbitalShuttlePad.Constructor(Vector3.Zero, obbasemesh, Vector3.Zero)).Build(context, hub) + assert(obj.isInstanceOf[OrbitalShuttlePad]) + assert(obj.HasGUID) + assert(obj.GUID == PlanetSideGUID(1)) + assert(obj == hub(1).get) + } + } +} + object ServerObjectBuilderTest { import net.psforever.objects.guid.source.MaxNumberSource def NumberPoolHub: NumberPoolHub = { diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala index 6cb0afedf..63342d6fa 100644 --- a/src/test/scala/objects/VehicleControlTest.scala +++ b/src/test/scala/objects/VehicleControlTest.scala @@ -1,7 +1,7 @@ // Copyright (c) 2020 PSForever package objects -import akka.actor.Props +import akka.actor.{ActorRef, Props} import akka.actor.typed.scaladsl.adapter._ import akka.testkit.TestProbe import base.{ActorTest, FreedContextActorTest} @@ -55,7 +55,7 @@ class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { vehicle.GUID = PlanetSideGUID(1) player1.GUID = PlanetSideGUID(2) - vehicle.Seats(1).Occupant = player1 //passenger seat + vehicle.Seats(1).mount(player1) //passenger mount player1.VehicleSeated = vehicle.GUID expectNoMessage(200 milliseconds) @@ -75,7 +75,7 @@ class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { } ) assert(player1.VehicleSeated.isEmpty) - assert(vehicle.Seats(1).Occupant.isEmpty) + assert(vehicle.Seats(1).occupant.isEmpty) } } } @@ -109,17 +109,16 @@ class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTes util().GUID = PlanetSideGUID(utilityId) utilityId += 1 } - vehicle.Seats(1).Occupant = player1 //passenger seat + vehicle.Seats(1).mount(player1) //passenger mount player1.VehicleSeated = vehicle.GUID - lodestar.Seats(0).Occupant = player2 + lodestar.Seats(0).mount(player2) player2.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = vehicle + lodestar.CargoHolds(1).mount(vehicle) vehicle.MountedIn = lodestar.GUID val vehicleProbe = new TestProbe(system) zone.VehicleEvents = vehicleProbe.ref zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this - expectNoMessage(200 milliseconds) "VehicleControl" should { "if mounted as cargo, self-eject when marked for deconstruction" in { @@ -196,7 +195,7 @@ class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTes } ) assert(player1.VehicleSeated.isEmpty) - assert(vehicle.Seats(1).Occupant.isEmpty) + assert(vehicle.Seats(1).occupant.isEmpty) } } } @@ -232,17 +231,16 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor util().GUID = PlanetSideGUID(utilityId) utilityId += 1 } - vehicle.Seats(1).Occupant = player1 //passenger seat + vehicle.Seats(1).mount(player1) //passenger mount player1.VehicleSeated = vehicle.GUID - lodestar.Seats(0).Occupant = player2 + lodestar.Seats(0).mount(player2) player2.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = vehicle + lodestar.CargoHolds(1).mount(vehicle) vehicle.MountedIn = lodestar.GUID val vehicleProbe = new TestProbe(system) zone.VehicleEvents = vehicleProbe.ref zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this - expectNoMessage(200 milliseconds) "VehicleControl" should { "if with mounted cargo, eject it when marked for deconstruction" in { @@ -260,7 +258,7 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor } ) assert(player2.VehicleSeated.isEmpty) - assert(lodestar.Seats(0).Occupant.isEmpty) + assert(lodestar.Seats(0).occupant.isEmpty) //cargo dismounting messages assert( vehicle_msg(1) match { @@ -323,95 +321,92 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor } class VehicleControlMountingBlockedExosuitTest extends ActorTest { - val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) + val catchallProbe = new TestProbe(system) + val catchall = catchallProbe.ref + val zone = new Zone("test", new ZoneMap("test-map"), 0) { + override def SetupNumberPools(): Unit = {} + override def AvatarEvents: ActorRef = catchall + override def LocalEvents: ActorRef = catchall + override def VehicleEvents: ActorRef = catchall + override def Activity: ActorRef = catchall + } + def checkCanNotMount(probe: TestProbe, id: String): Unit = { + val reply = probe.receiveOne(Duration.create(250, "ms")) reply match { case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) + assert(msg.response.isInstanceOf[Mountable.CanNotMount], s"test $id") case _ => - assert(false) + assert(false, s"test $id-b") } } - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) + def checkCanMount(probe: TestProbe, id: String): Unit = { + val reply = probe.receiveOne(Duration.create(250, "ms")) reply match { case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) + assert(msg.response.isInstanceOf[Mountable.CanMount], s" - test: $id") case _ => - assert(false) + assert(false, s" - test: $id-b") } } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) + vehicle.Zone = zone vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") val vehicle2 = Vehicle(GlobalDefinitions.lightning) vehicle2.Faction = PlanetSideEmpire.TR vehicle2.GUID = PlanetSideGUID(11) + vehicle2.Zone = zone vehicle2.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle2), "vehicle2-test") val player1 = Player(VehicleTest.avatar1) player1.ExoSuit = ExoSuitType.Reinforced player1.GUID = PlanetSideGUID(1) + player1.Zone = zone val player2 = Player(VehicleTest.avatar1) player2.ExoSuit = ExoSuitType.MAX player2.GUID = PlanetSideGUID(2) + player2.Zone = zone val player3 = Player(VehicleTest.avatar1) player3.ExoSuit = ExoSuitType.Agile player3.GUID = PlanetSideGUID(3) + player3.Zone = zone "Vehicle Control" should { - "block players from sitting if their exo-suit is not allowed by the seat - apc_tr" in { + "block players from sitting if their exo-suit is not allowed by the mount - apc_tr" in { + val probe = new TestProbe(system) // disallow - vehicle2.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) // Reinforced in non-reinforced seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) //MAX in non-Reinforced seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) //MAX in non-MAX seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player1, 9), probe.ref) //Reinforced in MAX-only seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player3, 9), probe.ref) //Agile in MAX-only seat - checkCanNotMount() + vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) //MAX in non-Max mount + checkCanNotMount(probe, "MAX in non-Max mount 1") + vehicle.Actor.tell(Mountable.TryMount(player2, 2), probe.ref) //MAX in non-MAX mount + checkCanNotMount(probe, "MAX in non-MAX mount 2") + vehicle.Actor.tell(Mountable.TryMount(player1, 11), probe.ref) //Reinforced in MAX-only mount + checkCanNotMount(probe, "Reinforced in MAX-only mount") + vehicle.Actor.tell(Mountable.TryMount(player3, 11), probe.ref) //Agile in MAX-only mount + checkCanNotMount(probe, "Agile in MAX-only mount") //allow - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) // Reinforced in driver seat allowing all except MAX - checkCanMount() - vehicle.Actor.tell(VehicleControl.AssignOwnership(None), probe.ref) // Reset ownership to allow further driver seat mounting tests - vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) // Reinforced in passenger seat allowing all except MAX - checkCanMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 9), probe.ref) // MAX in MAX-only seat - checkCanMount() - vehicle.Actor.tell(Mountable.TryMount(player3, 0), probe.ref) // Agile in driver seat allowing all except MAX - checkCanMount() + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) // Reinforced in driver mount allowing all except MAX + checkCanMount(probe, "Reinforced in driver mount allowing all except MAX") + // Reset to allow further driver mount mounting tests + vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) + probe.receiveOne(500 milliseconds) //discard + vehicle.Owner = None //ensure + vehicle.OwnerName = None //ensure + vehicle.Actor.tell(Mountable.TryMount(player3, 1), probe.ref) // Agile in driver mount allowing all except MAX + checkCanMount(probe, "Agile in driver mount allowing all except MAX") + vehicle.Actor.tell(Mountable.TryMount(player1, 3), probe.ref) // Reinforced in passenger mount allowing all except MAX + checkCanMount(probe, "Reinforced in passenger mount allowing all except MAX") + vehicle.Actor.tell(Mountable.TryMount(player2, 11), probe.ref) // MAX in MAX-only mount + checkCanMount(probe, "MAX in MAX-only mount") } } } class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -424,29 +419,20 @@ class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { "Vehicle Control" should { //11 June 2018: Group is not supported yet so do not bother testing it - "block players from sitting if the seat does not allow it" in { + "block players from sitting if the mount does not allow it" in { - vehicle.PermissionGroup(2, 3) //passenger group -> empire - vehicle.Actor.tell(Mountable.TryMount(player1, 3), probe.ref) //passenger seat - checkCanMount() - vehicle.PermissionGroup(2, 0) //passenger group -> locked - vehicle.Actor.tell(Mountable.TryMount(player2, 4), probe.ref) //passenger seat - checkCanNotMount() + vehicle.PermissionGroup(2, 3) //passenger group -> empire + vehicle.Actor.tell(Mountable.TryMount(player1, 4), probe.ref) //passenger mount + VehicleControlTest.checkCanMount(probe, "") + vehicle.PermissionGroup(2, 0) //passenger group -> locked + vehicle.Actor.tell(Mountable.TryMount(player2, 5), probe.ref) //passenger mount + VehicleControlTest.checkCanNotMount(probe, "") } } } class VehicleControlMountingDriverSeatTest extends ActorTest { val probe = new TestProbe(system) - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -455,38 +441,19 @@ class VehicleControlMountingDriverSeatTest extends ActorTest { player1.GUID = PlanetSideGUID(1) "Vehicle Control" should { - "allow players to sit in the driver seat, even if it is locked, if the vehicle is unowned" in { + "allow players to sit in the driver mount, even if it is locked, if the vehicle is unowned" in { assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) assert(vehicle.Owner.isEmpty) - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) } } } class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -497,36 +464,27 @@ class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { player2.GUID = PlanetSideGUID(2) "Vehicle Control" should { - "block players that are not the current owner from sitting in the driver seat (locked)" in { + "block players that are not the current owner from sitting in the driver mount (locked)" in { assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) vehicle.Owner = player1.GUID - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) probe.receiveOne(Duration.create(100, "ms")) //discard - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) - checkCanNotMount() - assert(vehicle.Seats(0).Occupant.isEmpty) + vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) + VehicleControlTest.checkCanNotMount(probe, "") + assert(vehicle.Seats(0).occupant.isEmpty) } } } class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { val probe = new TestProbe(system) - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -537,22 +495,22 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { player2.GUID = PlanetSideGUID(2) "Vehicle Control" should { - "allow players that are not the current owner to sit in the driver seat (empire)" in { + "allow players that are not the current owner to sit in the driver mount (empire)" in { vehicle.PermissionGroup(0, 3) //passenger group -> empire assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) vehicle.Owner = player1.GUID //owner set - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) probe.receiveOne(Duration.create(100, "ms")) //discard - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) } } } @@ -708,7 +666,7 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID player1.Actor = playerProbe.ref vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") @@ -763,7 +721,7 @@ class VehicleControlInteractWithWaterTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") @@ -830,7 +788,7 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID player1.Actor = playerProbe.ref vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") @@ -899,7 +857,7 @@ class VehicleControlInteractWithLavaTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") @@ -957,7 +915,7 @@ class VehicleControlInteractWithDeathTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") @@ -983,4 +941,25 @@ object VehicleControlTest { val avatar1 = Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val avatar2 = Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + + + def checkCanNotMount(probe: TestProbe, id: String): Unit = { + val reply = probe.receiveOne(Duration.create(250, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount], s"test $id") + case _ => + assert(false, s"test $id-b") + } + } + + def checkCanMount(probe: TestProbe, id: String): Unit = { + val reply = probe.receiveOne(Duration.create(250, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount], s" - test: $id") + case _ => + assert(false, s" - test: $id-b") + } + } } diff --git a/src/test/scala/objects/VehicleTest.scala b/src/test/scala/objects/VehicleTest.scala index cb5a860a5..87bd79c6a 100644 --- a/src/test/scala/objects/VehicleTest.scala +++ b/src/test/scala/objects/VehicleTest.scala @@ -2,7 +2,8 @@ package objects import net.psforever.objects._ -import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition} +import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.serverobject.mount._ import net.psforever.objects.vehicles._ import net.psforever.types.{PlanetSideGUID, _} import org.specs2.mutable._ @@ -12,35 +13,31 @@ class VehicleTest extends Specification { "SeatDefinition" should { val seat = new SeatDefinition - seat.ArmorRestriction = SeatArmorRestriction.MaxOnly - seat.Bailable = true - seat.ControlledWeapon = 5 + seat.restriction = MaxOnly + seat.bailable = true "define (default)" in { val t_seat = new SeatDefinition - t_seat.ArmorRestriction mustEqual SeatArmorRestriction.NoMax - t_seat.Bailable mustEqual false - t_seat.ControlledWeapon.isEmpty mustEqual true + t_seat.restriction mustEqual NoMax + t_seat.bailable mustEqual false } "define (custom)" in { - seat.ArmorRestriction mustEqual SeatArmorRestriction.MaxOnly - seat.Bailable mustEqual true - seat.ControlledWeapon.contains(5) + seat.restriction mustEqual MaxOnly + seat.bailable mustEqual true } } "VehicleDefinition" should { "define" in { val fury = GlobalDefinitions.fury - fury.CanBeOwned mustEqual true + fury.CanBeOwned.contains(true) mustEqual true fury.CanCloak mustEqual false fury.Seats.size mustEqual 1 - fury.Seats(0).Bailable mustEqual true - fury.Seats(0).ControlledWeapon.contains(1) + fury.Seats(0).bailable mustEqual true fury.MountPoints.size mustEqual 2 - fury.MountPoints.get(1).contains(0) - fury.MountPoints.get(2).contains(0) + fury.MountPoints.get(1).contains(MountInfo(0, Vector3(0,0,0))) mustEqual true + fury.MountPoints.get(2).contains(MountInfo(0, Vector3(0,0,0))) mustEqual true fury.Weapons.size mustEqual 1 fury.Weapons.get(0).isEmpty mustEqual true fury.Weapons.get(1).contains(GlobalDefinitions.fury_weapon_systema) @@ -52,69 +49,67 @@ class VehicleTest extends Specification { "Seat" should { val seat_def = new SeatDefinition - seat_def.ArmorRestriction = SeatArmorRestriction.MaxOnly - seat_def.Bailable = true - seat_def.ControlledWeapon = 5 + seat_def.restriction = MaxOnly + seat_def.bailable = true "construct" in { val seat = new Seat(seat_def) - seat.ArmorRestriction mustEqual SeatArmorRestriction.MaxOnly - seat.Bailable mustEqual true - seat.ControlledWeapon.contains(5) + seat.definition.restriction mustEqual MaxOnly + seat.bailable mustEqual true seat.isOccupied mustEqual false - seat.Occupant.isEmpty mustEqual true + seat.occupant.isEmpty mustEqual true } "player can sit" in { val seat = new Seat(seat_def) - seat.Occupant.isDefined mustEqual false + seat.isOccupied mustEqual false val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX - seat.Occupant = player1 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player1) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true } "one occupant at a time" in { val seat = new Seat(seat_def) - seat.Occupant.isDefined mustEqual false + seat.isOccupied mustEqual false val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX - seat.Occupant = player1 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player1) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true val player2 = Player(avatar1) player2.ExoSuit = ExoSuitType.MAX - seat.Occupant = player2 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player2) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true } - "one player must get out of seat before other can get in" in { + "one player must get out of mount before other can get in" in { val seat = new Seat(seat_def) - seat.Occupant.isDefined mustEqual false + seat.isOccupied mustEqual false val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX - seat.Occupant = player1 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player1) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true val player2 = Player(avatar2) player2.ExoSuit = ExoSuitType.MAX - seat.Occupant = player2 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player2) mustEqual false - seat.Occupant.contains(player1) mustEqual true + seat.mount(player2) + seat.isOccupied mustEqual true + seat.occupant.contains(player2) mustEqual false + seat.occupants.contains(player1) mustEqual true - seat.Occupant = None - seat.Occupant.isDefined mustEqual false - seat.Occupant = player2 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player2) mustEqual true + seat.unmount(player1) + seat.isOccupied mustEqual false + seat.mount(player2) + seat.isOccupied mustEqual true + seat.occupants.contains(player2) mustEqual true } } @@ -128,11 +123,10 @@ class VehicleTest extends Specification { val fury_vehicle = Vehicle(GlobalDefinitions.fury) fury_vehicle.Owner.isEmpty mustEqual true fury_vehicle.Seats.size mustEqual 1 - fury_vehicle.Seats(0).ArmorRestriction mustEqual SeatArmorRestriction.NoMax + fury_vehicle.Seats(0).definition.restriction mustEqual NoMax fury_vehicle.Seats(0).isOccupied mustEqual false - fury_vehicle.Seats(0).Occupant.isEmpty mustEqual true - fury_vehicle.Seats(0).Bailable mustEqual true - fury_vehicle.Seats(0).ControlledWeapon.contains(1) + fury_vehicle.Seats(0).occupants.isEmpty mustEqual true + fury_vehicle.Seats(0).bailable mustEqual true fury_vehicle.PermissionGroup(0).contains(VehicleLockState.Locked) //driver fury_vehicle.PermissionGroup(1).contains(VehicleLockState.Empire) //gunner fury_vehicle.PermissionGroup(2).contains(VehicleLockState.Empire) //passenger @@ -180,7 +174,7 @@ class VehicleTest extends Specification { fury_vehicle.Owner.contains(PlanetSideGUID(2)) mustEqual true } - "can use mount point to get seat number" in { + "can use mount point to get mount number" in { val fury_vehicle = Vehicle(GlobalDefinitions.fury) fury_vehicle.GetSeatFromMountPoint(0).isEmpty mustEqual true fury_vehicle.GetSeatFromMountPoint(1).contains(0) @@ -224,7 +218,7 @@ class VehicleTest extends Specification { fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id) mustEqual fury_vehicle.PermissionGroup(10) } - "can determine permission group from seat" in { + "can determine permission group from mount" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) harasser_vehicle.SeatPermissionGroup(0).contains(AccessPermissionGroup.Driver) harasser_vehicle.SeatPermissionGroup(1).contains(AccessPermissionGroup.Gunner) @@ -232,23 +226,23 @@ class VehicleTest extends Specification { //TODO test for AccessPermissionGroup.Passenger later } - "can find a passenger in a seat" in { + "can find a passenger in a mount" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) val player1 = Player(avatar1) player1.GUID = PlanetSideGUID(1) val player2 = Player(avatar2) player2.GUID = PlanetSideGUID(2) - harasser_vehicle.Seat(0).get.Occupant = player1 //don't worry about ownership for now - harasser_vehicle.Seat(1).get.Occupant = player2 + harasser_vehicle.Seat(0).get.mount(player1) //don't worry about ownership for now + harasser_vehicle.Seat(1).get.mount(player2) harasser_vehicle.PassengerInSeat(player1).contains(0) harasser_vehicle.PassengerInSeat(player2).contains(1) - harasser_vehicle.Seat(0).get.Occupant = None + harasser_vehicle.Seat(0).get.unmount(player1) harasser_vehicle.PassengerInSeat(player1).isEmpty mustEqual true harasser_vehicle.PassengerInSeat(player2).contains(1) } - "can find a weapon controlled from seat" in { + "can find a weapon controlled from mount" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) val chaingun_p = harasser_vehicle.Weapons(2).Equipment chaingun_p.isDefined mustEqual true diff --git a/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala b/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala index 8e559766d..4e1da10a7 100644 --- a/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala +++ b/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala @@ -5,14 +5,12 @@ import akka.actor.{ActorSystem, Props} import base.ActorTest import net.psforever.objects.avatar.Avatar import net.psforever.objects.{Default, GlobalDefinitions, Player} -import net.psforever.objects.definition.SeatDefinition import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalMech, ImplantTerminalMechControl} +import net.psforever.objects.serverobject.mount.{MountInfo, Mountable, Seat, SeatDefinition} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.Terminal -import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalMech, ImplantTerminalMechControl} -import net.psforever.objects.vehicles.Seat import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification @@ -24,14 +22,10 @@ class ImplantTerminalMechTest extends Specification { "define" in { val implant_terminal_mech = GlobalDefinitions.implant_terminal_mech implant_terminal_mech.ObjectId mustEqual 410 - implant_terminal_mech.MountPoints mustEqual Map(1 -> 0) + implant_terminal_mech.MountPoints.get(1).contains(MountInfo(0, Vector3.Zero)) mustEqual true implant_terminal_mech.Seats.keySet mustEqual Set(0) implant_terminal_mech.Seats(0).isInstanceOf[SeatDefinition] mustEqual true - implant_terminal_mech - .Seats(0) - .ArmorRestriction mustEqual net.psforever.objects.vehicles.SeatArmorRestriction.NoMax - implant_terminal_mech.Seats(0).Bailable mustEqual false - implant_terminal_mech.Seats(0).ControlledWeapon.isEmpty mustEqual true + implant_terminal_mech.Seats(0).bailable mustEqual false } } @@ -44,20 +38,20 @@ class ImplantTerminalMechTest extends Specification { obj.Seats(0).isInstanceOf[Seat] mustEqual true } - "get seat from mount points" in { + "get mount from mount points" in { val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) obj.GetSeatFromMountPoint(0).isEmpty mustEqual true obj.GetSeatFromMountPoint(1).contains(0) mustEqual true obj.GetSeatFromMountPoint(2).isEmpty mustEqual true } - "get passenger in a seat" in { + "get passenger in a mount" in { val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) obj.PassengerInSeat(player).isEmpty mustEqual true - obj.Seats(0).Occupant = player + obj.Seats(0).mount(player) obj.PassengerInSeat(player).contains(0) mustEqual true - obj.Seats(0).Occupant = None + obj.Seats(0).unmount(player) obj.PassengerInSeat(player).isEmpty mustEqual true } } @@ -77,7 +71,7 @@ class ImplantTerminalMechControl2Test extends ActorTest { "ImplantTerminalMechControl" should { "let a player mount" in { val (player, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - val msg = Mountable.TryMount(player, 0) + val msg = Mountable.TryMount(player, 1) mech.Actor ! msg val reply = receiveOne(Duration.create(200, "ms")) @@ -87,7 +81,7 @@ class ImplantTerminalMechControl2Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanMount] assert(reply3.obj == mech) - assert(reply3.seat_num == 0) + assert(reply3.seat_number == 0) } } } @@ -99,10 +93,10 @@ class ImplantTerminalMechControl3Test extends ActorTest { val (player1, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - mech.Actor ! Mountable.TryMount(player1, 0) + mech.Actor ! Mountable.TryMount(player1, 1) receiveOne(Duration.create(100, "ms")) //consume reply - mech.Actor ! Mountable.TryMount(player2, 0) + mech.Actor ! Mountable.TryMount(player2, 1) val reply = receiveOne(Duration.create(100, "ms")) assert(reply.isInstanceOf[Mountable.MountMessages]) val reply2 = reply.asInstanceOf[Mountable.MountMessages] @@ -110,7 +104,7 @@ class ImplantTerminalMechControl3Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanNotMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanNotMount] assert(reply3.obj == mech) - assert(reply3.seat_num == 0) + assert(reply3.mount_point == 1) } } } @@ -119,7 +113,7 @@ class ImplantTerminalMechControl4Test extends ActorTest { "ImplantTerminalMechControl" should { "dismount player after mounting" in { val (player, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - mech.Actor ! Mountable.TryMount(player, 0) + mech.Actor ! Mountable.TryMount(player, 1) receiveOne(Duration.create(200, "ms")) //consume reply assert(mech.Seat(0).get.isOccupied) @@ -141,11 +135,11 @@ class ImplantTerminalMechControl5Test extends ActorTest { "ImplantTerminalMechControl" should { "block a player from dismounting" in { val (player, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - mech.Actor ! Mountable.TryMount(player, 0) + mech.Actor ! Mountable.TryMount(player, 1) receiveOne(Duration.create(100, "ms")) //consume reply assert(mech.Seat(0).get.isOccupied) - mech.Velocity = Vector3(1, 0, 0) //makes no sense, but it works as the "seat" is not bailable + mech.Velocity = Vector3(1, 0, 0) //makes no sense, but it works as the "mount" is not bailable mech.Actor ! Mountable.TryDismount(player, 0) val reply = receiveOne(Duration.create(100, "ms")) assert(reply.isInstanceOf[Mountable.MountMessages]) diff --git a/src/test/scala/service/HartServiceTest.scala b/src/test/scala/service/HartServiceTest.scala new file mode 100644 index 000000000..3952a8f7a --- /dev/null +++ b/src/test/scala/service/HartServiceTest.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2021 PSForever +package service + +import akka.actor.{ActorRef, Props} +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.services.hart.{HartService, HartTimer} +import net.psforever.types.PlanetSideGUID + +import scala.concurrent.duration._ + +class HartServiceTest extends ActorTest { + "HartService" should { + val hart = system.actorOf(Props[HartService](), name = "hart") + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), zoneNumber = 0) { + override def SetupNumberPools(): Unit = {} + override def AvatarEvents: ActorRef = catchall + override def LocalEvents: ActorRef = catchall + override def VehicleEvents: ActorRef = catchall + override def Activity: ActorRef = catchall + } + + "pass messages back upon pairing" in { + val probe = new TestProbe(system) + hart ! HartTimer.PairWith(zone, PlanetSideGUID(1), PlanetSideGUID(2), probe.ref) + probe.receiveOne(max = 1 seconds) match { + case HartTimer.ShuttleDocked("test") => assert(true) + case _ => assert(false) + } + } + } +} + +object HartServiceTest { /* initially left empty */ } diff --git a/src/test/scala/service/HartTimerTest.scala b/src/test/scala/service/HartTimerTest.scala new file mode 100644 index 000000000..038909b17 --- /dev/null +++ b/src/test/scala/service/HartTimerTest.scala @@ -0,0 +1,57 @@ +// Copyright (c) 2021 PSForever +package service + +import akka.actor.Props +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.services.hart.HartTimer +import net.psforever.types.PlanetSideGUID + +import scala.concurrent.duration._ + +class HartTimerNotScheduled extends ActorTest { + "HartTimer" should { + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + } + val timer = system.actorOf(Props(classOf[HartTimer], zone), "hart-timer") + + "not do anything if paired before having a schedule set" in { + val probe = new TestProbe(system) + timer ! HartTimer.PairWith(zone, PlanetSideGUID(1), PlanetSideGUID(2), probe.ref) + probe.expectNoMessage(max = 3 seconds) + } + } +} + +class HartTimerInitializedPairingScheduled extends ActorTest { + "HartTimer" should { + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + } + val timer = system.actorOf(Props(classOf[HartTimer], zone), "hart-timer") + + "perform some initialization when paired" in { + val probe = new TestProbe(system) + timer ! HartTimer.SetEventDurations("test", 55000, 10000) + timer ! HartTimer.PairWith(zone, PlanetSideGUID(1), PlanetSideGUID(2), probe.ref) + probe.receiveOne(1 seconds) match { + case HartTimer.ShuttleDocked("test") => assert(true) + case _ => assert(false) + } + } + } +} + +object HartTimerTest { /* initially left empty */ }