* initial OrbitalShuttleTimeMsg packet and tests; new objects to support HART shuttle transport system

* master was stale

* grouped scheduling for timing orbital shuttle activity

* door lock controls for HART shuttle lifecycle, and specifically for the doors that lead into the shuttle boarding hallway

* separation of the door from the door unlocking logic, which now has to be provided if performed by an outside source; a door that is locked either by bolt, HART routine, or other reason, can now be shut immediately; message when HART is not docked with a corresponding entry hallway door

* better degree of door logic control; all shuttle-related messages have been moved to LocalService; careful managing of 'original state' for the shuttle's cycle

* modification of seat mounting and cargo mounting support entities to expand functionality

* absolutely very little to do with the feature of this branch and a lot to do with yak-shaving; long story short, class inheritance is greatly modified and mountable seats can now accept multiple players if initialized properly

* a lot has changed: distribution of MountableBehavior, mount point information is more complex, vehicles convert differently, the routine of the shuttle timer is initialized differently; you can now successfully utilize the HART shuttle to drop into a zone

* swap of shutle from pad to pad control; tests and comments

* eject players from HART gantry hallway as if passengers dismounting from seat when not boarding through the use of environmental geometry; HART system uses duration from config settings to set scheduler

* rebase to curious master; repairs to vector rotation calculations; regression of mountable changes involving seats with occupancy greater than 1; orbital shuttle as a unique vehicle and amenity; corrected dismount offsets and offset calculations; weird angle of nc hart a building has been properly accommodated; hart events have prerequisite animation states

* rebase with master; looks like rebase with merged_master, which is also a commit

* lots of tests (though not nearly enough); checking the permission group of a shuttle seat no longer creates that seat

* fixing explosions

* fixed the persistence monitor service potentially using non-printable unicode in actor names

* can not use a droppod to gain access to one's own sanctuary

* removed hart facility update that causing open bay doors and beeping

* PR review changes

* fix for aggravation issues
This commit is contained in:
Fate-JH 2021-03-23 09:44:10 -04:00 committed by GitHub
parent e3de497be3
commit 71ab35ecab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 4672 additions and 1870 deletions

View file

@ -21,6 +21,7 @@ ignore:
- "src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala" - "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/pad/AutoDriveControls.scala"
- "src/main/scala/net/psforever/objects/serverobject/structures/StructureType.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/turret/TurretUpgrade.scala"
- "src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala" - "src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala"
- "src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.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/avatar/AvatarResponse.scala"
- "src/main/scala/net/psforever/services/galaxy/GalaxyAction.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/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/LocalAction.scala"
- "src/main/scala/net/psforever/services/local/LocalResponse.scala" - "src/main/scala/net/psforever/services/local/LocalResponse.scala"
- "src/main/scala/net/psforever/services/vehicle/VehicleAction.scala" - "src/main/scala/net/psforever/services/vehicle/VehicleAction.scala"

View file

@ -36,6 +36,7 @@ import org.slf4j
import scopt.OParser import scopt.OParser
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.packet.PlanetSidePacket import net.psforever.packet.PlanetSidePacket
import net.psforever.services.hart.HartService
object Server { object Server {
private val logger = org.log4s.getLogger private val logger = org.log4s.getLogger
@ -129,6 +130,7 @@ object Server {
serviceManager ! ServiceManager.Register(classic.Props[SquadService](), "squad") serviceManager ! ServiceManager.Register(classic.Props[SquadService](), "squad")
serviceManager ! ServiceManager.Register(classic.Props[AccountPersistenceService](), "accountPersistence") serviceManager ! ServiceManager.Register(classic.Props[AccountPersistenceService](), "accountPersistence")
serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager") 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.login.port), login), "login-socket")
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket") system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket")

View file

@ -203,7 +203,7 @@ class AutoRepairFacilityIntegrationAntGiveNtuTest extends FreedContextActorTest
ant.NtuCapacitor = maxNtuCap ant.NtuCapacitor = maxNtuCap
ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant")
ant.Zone = zone ant.Zone = zone
ant.Seats(0).Occupant = player ant.Seats(0).mount(player)
ant.DeploymentState = DriveState.Deployed ant.DeploymentState = DriveState.Deployed
building.Amenities = terminal building.Amenities = terminal
building.Amenities = silo building.Amenities = silo
@ -297,7 +297,7 @@ class AutoRepairFacilityIntegrationTerminalDestroyedTerminalAntTest extends Free
ant.NtuCapacitor = maxNtuCap ant.NtuCapacitor = maxNtuCap
ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant")
ant.Zone = zone ant.Zone = zone
ant.Seats(0).Occupant = player ant.Seats(0).mount(player)
ant.DeploymentState = DriveState.Deployed ant.DeploymentState = DriveState.Deployed
building.Amenities = terminal building.Amenities = terminal
building.Amenities = silo building.Amenities = silo
@ -399,7 +399,7 @@ class AutoRepairFacilityIntegrationTerminalIncompleteRepairTest extends FreedCon
ant.NtuCapacitor = maxNtuCap ant.NtuCapacitor = maxNtuCap
ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant")
ant.Zone = zone ant.Zone = zone
ant.Seats(0).Occupant = player ant.Seats(0).mount(player)
ant.DeploymentState = DriveState.Deployed ant.DeploymentState = DriveState.Deployed
building.Amenities = terminal building.Amenities = terminal
building.Amenities = silo building.Amenities = silo

View file

@ -39,7 +39,7 @@ class VehicleSpawnControl2Test extends ActorTest {
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.LoadVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.LoadVehicle])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) 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.PlayerSeatedInVehicle])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ServerVehicleOverrideStart]) 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.LoadVehicle])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) 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.PlayerSeatedInVehicle])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails])
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ServerVehicleOverrideStart]) 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 //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 //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 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) vehicle.Position = Vector3(12, 0, 0)
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ResetSpawnPad]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ResetSpawnPad])
probe.expectMsgClass(3 seconds, classOf[VehicleSpawnPad.ConcealPlayer]) probe.expectMsgClass(3 seconds, classOf[VehicleSpawnPad.ConcealPlayer])

View file

@ -82,6 +82,15 @@ game {
# Modify the amount of NTU drain per autorepair tick for facility amenities # Modify the amount of NTU drain per autorepair tick for facility amenities
amenity-autorepair-drain-rate = 0.5 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 { new-avatar {
# Starting battle rank # Starting battle rank
br = 1 br = 1

View file

@ -420,7 +420,7 @@ class MiddlewareActor(
case Successful((packet, None)) => case Successful((packet, None)) =>
in(packet) in(packet)
case Failure(e) => case Failure(e) =>
log.error(s"could not decode packet: $e") log.error(s"Could not decode packet: $e")
} }
Behaviors.same Behaviors.same
@ -530,7 +530,7 @@ class MiddlewareActor(
def in(packet: Attempt[PlanetSidePacket]): Unit = { def in(packet: Attempt[PlanetSidePacket]): Unit = {
packet match { packet match {
case Successful(_packet) => in(_packet) 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 _ => case _ =>
PacketCoding.encodePacket(packet) match { PacketCoding.encodePacket(packet) match {
case Successful(payload) => outQueue.enqueue((packet, payload)) 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)) outQueueBundled.enqueue(smp(slot = 0, data.bytes))
sendFirstBundle() sendFirstBundle()
case Failure(cause) => 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 //to avoid packets being lost, unwrap bundle and queue the packets individually
bundle.foreach { packet => bundle.foreach { packet =>
outQueueBundled.enqueue(smp(slot = 0, packet.bytes)) outQueueBundled.enqueue(smp(slot = 0, packet.bytes))
@ -626,7 +626,7 @@ class MiddlewareActor(
} }
} catch { } catch {
case e: Throwable => 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) => case Successful(data) =>
data.grouped((MTU - 8) * 8).map(vec => smp(slot = 4, vec.bytes)).toSeq data.grouped((MTU - 8) * 8).map(vec => smp(slot = 4, vec.bytes)).toSeq
case Failure(cause) => case Failure(cause) =>
log.error(cause.message) log.error(s"Could not split packet: ${cause.message}")
Seq() Seq()
} }
} else { } else {

View file

@ -1,6 +1,5 @@
package net.psforever.actors.zone package net.psforever.actors.zone
import akka.actor.Actor
import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} 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.NtuContainer
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} 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.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} 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.objects.zones.Zone
import net.psforever.persistence import net.psforever.persistence
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}

View file

@ -236,9 +236,10 @@ object ExplosiveDeployableControl {
def detectTarget(g1: Geometry3D, up: Vector3)(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float) : Boolean = { def detectTarget(g1: Geometry3D, up: Vector3)(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float) : Boolean = {
val g2 = obj2.Definition.Geometry(obj2) val g2 = obj2.Definition.Geometry(obj2)
val dir = g2.center.asVector3 - g1.center.asVector3 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 point1 = g1.pointOnOutside(dir).asVector3
val point2 = g2.pointOnOutside(Vector3.neg(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) && (scalar >= 0 || Vector3.MagnitudeSquared(up * scalar) < 0.35f) &&
math.min( math.min(
Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3), Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3),

File diff suppressed because it is too large Load diff

View file

@ -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.Damageable.Target
import net.psforever.objects.serverobject.damage.DamageableWeaponTurret import net.psforever.objects.serverobject.damage.DamageableWeaponTurret
import net.psforever.objects.serverobject.hackable.Hackable 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.repair.RepairableWeaponTurret
import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret} import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret}
import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.damage.DamageCalculations
@ -25,8 +25,6 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
with Hackable { with Hackable {
WeaponTurret.LoadDefinition(this) WeaponTurret.LoadDefinition(this)
def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap
override def Definition = tdef override def Definition = tdef
} }
@ -65,8 +63,7 @@ class TurretControl(turret: TurretDeployable)
extends Actor extends Actor
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events
with MountableBehavior.TurretMount with MountableBehavior
with MountableBehavior.Dismount
with DamageableWeaponTurret with DamageableWeaponTurret
with RepairableWeaponTurret { with RepairableWeaponTurret {
def MountableObject = turret def MountableObject = turret
@ -91,6 +88,13 @@ class TurretControl(turret: TurretDeployable)
case _ => ; 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 = { override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause) super.DestructionAwareness(target, cause)
Deployables.AnnounceDestroyDeployable(turret, None) Deployables.AnnounceDestroyDeployable(turret, None)

View file

@ -1,10 +1,10 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.definition.{SeatDefinition, ToolDefinition, VehicleDefinition} import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} 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.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.aura.AuraContainer 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.objects.vital.resolution.DamageResistanceModel
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.annotation.tailrec
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
import scala.util.{Success, Try} 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. * 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 * 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. * 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. * Negative indices are expected to be excluded from this conversion.
* The value of the negative index does not have a specific meaning.<br> * The value of the negative index does not have a specific meaning.<br>
* <br> * <br>
@ -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 * 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. * 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 * 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" * 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 seat. * 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 seat. * 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) * 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. * 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.)<br> * (They also lose ownership when they leave the game, of course.)<br>
* <br> * <br>
* All seats have vehicle-level properties on top of their own internal properties. * 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 * A mount 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. * 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. * 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" group has already been mentioned and is usually composed of a single mount, the "first" one.
* The driver seat is typically locked to the person who can sit in it - the owner - unless manually unlocked. * The driver mount 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 mount besides the "driver" that has a weapon controlled from the mount is called a "gunner" seats.
* Any other seat besides the "driver" seat and "gunner" seats is called a "passenger" seat. * Any other mount besides the "driver" mount and "gunner" seats is called a "passenger" mount.
* All of these seats are typically unlocked normally. * 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, * The categories all have their own glyphs,
* sharing a red cross glyph as a "can not access" state, * sharing a red cross glyph as a "can not access" state,
* and may also use their lack of visibility to express 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` * @see `Vehicle.EquipmentUtilities`
* @param vehicleDef the vehicle's definition entry; * @param vehicleDef the vehicle's definition entry;
* stores and unloads pertinent information about the `Vehicle`'s configuration; * 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) class Vehicle(private val vehicleDef: VehicleDefinition)
extends AmenityOwner extends AmenityOwner
with MountableWeapons
with InteractsWithZoneEnvironment with InteractsWithZoneEnvironment
with Hackable with Hackable
with FactionAffinity with FactionAffinity
with Mountable
with MountedWeapons
with Deployment with Deployment
with Vitality with Vitality
with OwnableByPlayer with OwnableByPlayer
@ -90,19 +88,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
private var decal: Int = 0 private var decal: Int = 0
private var trunkAccess: Option[PlanetSideGUID] = None private var trunkAccess: Option[PlanetSideGUID] = None
private var jammered: Boolean = false private var jammered: Boolean = false
private var cloaked: Boolean = false private var cloaked: Boolean = false
private var flying: Boolean = false private var flying: Option[Int] = None
private var capacitor: Int = 0 private var capacitor: Int = 0
/** /**
* Permissions control who gets to access different parts of the vehicle; * 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] = private val groupPermissions: Array[VehicleLockState.Value] =
Array(VehicleLockState.Locked, VehicleLockState.Empire, VehicleLockState.Empire, VehicleLockState.Locked) 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 cargoHolds: Map[Int, Cargo] = Map.empty
private var weapons: Map[Int, EquipmentSlot] = Map.empty
private var utilities: Map[Int, Utility] = Map() private var utilities: Map[Int, Utility] = Map()
private val trunk: GridInventory = GridInventory() private val trunk: GridInventory = GridInventory()
@ -198,9 +195,13 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
Cloaked 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 = isFlying
Flying Flying
} }
@ -226,17 +227,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
Capacitor 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? * What are the access permissions for a position on this vehicle, seats or trunk?
* @param group the group index * @param group the group index
@ -291,24 +281,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
None 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] = { def CargoHold(cargoNumber: Int): Option[Cargo] = {
if (cargoNumber >= 0) { if (cargoNumber >= 0) {
this.cargoHolds.get(cargoNumber) this.cargoHolds.get(cargoNumber)
@ -322,12 +294,12 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
} }
def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = { def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = {
if (seatNumber == 0) { if (seatNumber == 0) { //valid in almost all cases
Some(AccessPermissionGroup.Driver) Some(AccessPermissionGroup.Driver)
} else { } else {
Seat(seatNumber) match { Seat(seatNumber) match {
case Some(seat) => case Some(_) =>
seat.ControlledWeapon match { Definition.controlledWeapons.get(seatNumber) match {
case Some(_) => case Some(_) =>
Some(AccessPermissionGroup.Gunner) Some(AccessPermissionGroup.Gunner)
case None => case None =>
@ -336,50 +308,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
case None => case None =>
CargoHold(seatNumber) match { CargoHold(seatNumber) match {
case Some(_) => case Some(_) =>
Some(AccessPermissionGroup.Passenger) Some(AccessPermissionGroup.Passenger) //TODO confirm this
case None => 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 def Utilities: Map[Int, Utility] = utilities
/** /**
@ -415,7 +355,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
def Inventory: GridInventory = trunk def Inventory: GridInventory = trunk
def VisibleSlots: Set[Int] = weapons.keySet def VisibleSlots: Set[Int] = weapons.keys.toSet
override def Slot(slotNum: Int): EquipmentSlot = { override def Slot(slotNum: Int): EquipmentSlot = {
weapons weapons
@ -535,7 +475,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
def PrepareGatingManifest(): VehicleManifest = { def PrepareGatingManifest(): VehicleManifest = {
val manifest = VehicleManifest(this) 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) vehicleGatingManifest = Some(manifest)
previousVehicleGatingManifest = None previousVehicleGatingManifest = None
manifest manifest
@ -676,12 +616,12 @@ object Vehicle {
//create seats //create seats
vehicle.seats = vdef.Seats.map[Int, Seat] { vehicle.seats = vdef.Seats.map[Int, Seat] {
case (num: Int, definition: SeatDefinition) => case (num: Int, definition: SeatDefinition) =>
num -> Seat(definition) num -> new Seat(definition)
}.toMap }.toMap
// create cargo holds // create cargo holds
vehicle.cargoHolds = vdef.Cargo.map[Int, Cargo] { vehicle.cargoHolds = vdef.Cargo.map[Int, Cargo] {
case (num, definition) => case (num, definition) =>
num -> Cargo(definition) num -> new Cargo(definition)
}.toMap }.toMap
//create utilities //create utilities
vehicle.utilities = vdef.Utilities.map[Int, Utility] { vehicle.utilities = vdef.Utilities.map[Int, Utility] {

View file

@ -87,7 +87,7 @@ object Vehicles {
/** /**
* Disassociate a player from a vehicle that he owns. * Disassociate a player from a vehicle that he owns.
* The vehicle must exist in the game world on the specified continent. * 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. * This is the player side of vehicle ownership removal.
* @param player the player * @param player the player
*/ */
@ -96,7 +96,7 @@ object Vehicles {
/** /**
* Disassociate a player from a vehicle that he owns. * Disassociate a player from a vehicle that he owns.
* The vehicle must exist in the game world on the specified continent. * 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. * This is the player side of vehicle ownership removal.
* @param player the player * @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. * 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. * then reload them for all clients.
* This is the vehicle side of vehicle ownership removal. * This is the vehicle side of vehicle ownership removal.
* @param player the player * @param player the player
@ -196,7 +196,7 @@ object Vehicles {
val manifestPassengerResults = manifestPassengers.map { name => vzone.Players.exists(_.name.equals(name)) } val manifestPassengerResults = manifestPassengers.map { name => vzone.Players.exists(_.name.equals(name)) }
manifestPassengerResults.forall(_ == true) && manifestPassengerResults.forall(_ == true) &&
vehicle.CargoHolds.values 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) .forall(_ == true)
case _ => case _ =>
false false
@ -230,18 +230,18 @@ object Vehicles {
val zone = target.Zone val zone = target.Zone
// Forcefully dismount any cargo // Forcefully dismount any cargo
target.CargoHolds.values.foreach(cargoHold => { target.CargoHolds.values.foreach(cargoHold => {
cargoHold.Occupant match { cargoHold.occupant match {
case Some(cargo: Vehicle) => case Some(cargo: Vehicle) =>
cargo.Seats(0).Occupant match { cargo.Seats(0).occupant match {
case Some(_: Player) => case Some(_: Player) =>
CargoBehavior.HandleVehicleCargoDismount( CargoBehavior.HandleVehicleCargoDismount(
target.Zone, target.Zone,
cargo.GUID, cargo.GUID,
bailed = target.Flying, bailed = target.isFlying,
requestedByPassenger = false, requestedByPassenger = false,
kicked = true kicked = true
) )
case None => case _ =>
log.error("FinishHackingVehicle: vehicle in cargo hold missing driver") log.error("FinishHackingVehicle: vehicle in cargo hold missing driver")
CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, target.GUID, target, bailed = false, requestedByPassenger = false, kicked = true) 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 // Forcefully dismount all seated occupants from the vehicle
target.Seats.values.foreach(seat => { target.Seats.values.foreach(seat => {
seat.Occupant match { seat.occupant match {
case Some(tplayer) => case Some(tplayer: Player) =>
seat.Occupant = None seat.unmount(tplayer)
tplayer.VehicleSeated = None tplayer.VehicleSeated = None
if (tplayer.HasGUID) { if (tplayer.HasGUID) {
zone.VehicleEvents ! VehicleServiceMessage( zone.VehicleEvents ! VehicleServiceMessage(
@ -260,11 +260,11 @@ object Vehicles {
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID) 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 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? // 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() target.Actor ! Vehicle.Deconstruct()
} else { // Otherwise handle ownership transfer as normal } else { // Otherwise handle ownership transfer as normal
@ -407,4 +407,21 @@ object Vehicles {
case _ => ; 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)
}
} }

View file

@ -27,9 +27,11 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.objects.locker.LockerContainerControl import net.psforever.objects.locker.LockerContainerControl
import net.psforever.objects.serverobject.environment._ 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.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.services.hart.ShuttleState
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -60,6 +62,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath)
SetInteraction(EnvironmentAttribute.GantryDenialField, doInteractingWithGantryField)
SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater)
private[this] val log = org.log4s.getLogger(player.Name) 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 ) //align client interface fields with state
zone.GUID(target.VehicleSeated) match { zone.GUID(target.VehicleSeated) match {
case Some(obj: Mountable) => case Some(obj: Mountable) =>
//boot cadaver from seat internally (vehicle perspective) //boot cadaver from mount internally (vehicle perspective)
obj.PassengerInSeat(target) match { obj.PassengerInSeat(target) match {
case Some(index) => case Some(index) =>
obj.Seats(index).Occupant = None obj.Seats(index).unmount(target)
case _ => ; case _ => ;
} }
//boot cadaver from seat on client //boot cadaver from mount on client
events ! AvatarServiceMessage( events ! AvatarServiceMessage(
nameChannel, nameChannel,
AvatarAction.SendResponse( AvatarAction.SendResponse(
@ -1046,6 +1049,38 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
suicide() 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. * 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. * The player does have to endure a recovery period to get back to normal, though.

View file

@ -1,35 +1,14 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2021 PSForever
package net.psforever.objects.definition 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}
/** class CargoDefinition extends MountableSpaceDefinition[Vehicle] {
* 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
Name = "cargo" Name = "cargo"
def occupancy: Int = 1
def CargoRestriction: CargoVehicleRestriction.Value = { var restriction: MountRestriction[Vehicle] = LargeCargo
this.vehicleRestriction
}
def CargoRestriction_=(restriction: CargoVehicleRestriction.Value): CargoVehicleRestriction.Value = { var bailable: Boolean = true
this.vehicleRestriction = restriction
restriction
}
def Bailable: Boolean = {
this.bailable
}
def Bailable_=(canBail: Boolean): Boolean = {
this.bailable = canBail
canBail
}
} }

View file

@ -89,9 +89,9 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti
private var serverGeometry: Any => Geometry3D = GeometryForm.representByPoint() private var serverGeometry: Any => Geometry3D = GeometryForm.representByPoint()
def Geometry: Any => Geometry3D = if (ServerSplashTargetsCentroid) { def Geometry: Any => Geometry3D = if (ServerSplashTargetsCentroid) {
serverGeometry
} else {
GeometryForm.representByPoint() GeometryForm.representByPoint()
} else {
serverGeometry
} }
def Geometry_=(func: Any => Geometry3D): Any => Geometry3D = { def Geometry_=(func: Any => Geometry3D): Any => Geometry3D = {

View file

@ -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
}
}

View file

@ -4,7 +4,7 @@ package net.psforever.objects.definition
import net.psforever.objects.NtuContainerDefinition import net.psforever.objects.NtuContainerDefinition
import net.psforever.objects.definition.converter.VehicleConverter import net.psforever.objects.definition.converter.VehicleConverter
import net.psforever.objects.inventory.InventoryTile 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._
import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.resistance.ResistanceProfileMutators import net.psforever.objects.vital.resistance.ResistanceProfileMutators
@ -20,19 +20,14 @@ import scala.concurrent.duration._
*/ */
class VehicleDefinition(objectId: Int) class VehicleDefinition(objectId: Int)
extends ObjectDefinition(objectId) extends ObjectDefinition(objectId)
with MountableWeaponsDefinition
with VitalityDefinition with VitalityDefinition
with NtuContainerDefinition with NtuContainerDefinition
with ResistanceProfileMutators with ResistanceProfileMutators
with DamageResistanceModel { with DamageResistanceModel {
/** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */ /** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */
private var maxShields: Int = 0 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]() 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 var deployment: Boolean = false
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap() private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
private val utilityOffsets: mutable.HashMap[Int, Vector3] = 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 trunkLocation: Vector3 = Vector3.Zero
private var canCloak: Boolean = false private var canCloak: Boolean = false
private var canFly: 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<br>
* `Some(true)` - assign ownership upon the driver mount, maintains ownership after the driver dismounts<br>
* `Some(false)` - assign ownership upon the driver mount, becomes unowned after the driver dismounts<br>
* `None` - does not assign ownership<br>
* 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) private var serverVehicleOverrideSpeeds: (Int, Int) = (0, 0)
var undergoesDecay: Boolean = true
private var deconTime: Option[FiniteDuration] = None private var deconTime: Option[FiniteDuration] = None
private var maxCapacitor: Int = 0 private var maxCapacitor: Int = 0
private var destroyedModel: Option[DestroyedVehicle.Value] = None private var destroyedModel: Option[DestroyedVehicle.Value] = None
@ -64,15 +67,13 @@ class VehicleDefinition(objectId: Int)
MaxShields MaxShields
} }
def Seats: mutable.HashMap[Int, SeatDefinition] = seats
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo 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 = ownable
CanBeOwned CanBeOwned
} }
@ -91,8 +92,6 @@ class VehicleDefinition(objectId: Int)
CanFly CanFly
} }
def Weapons: mutable.HashMap[Int, ToolDefinition] = weapons
def Deployment: Boolean = deployment def Deployment: Boolean = deployment
def Deployment_=(deployable: Boolean): Boolean = { def Deployment_=(deployable: Boolean): Boolean = {

View file

@ -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)"))
}

View file

@ -2,7 +2,7 @@
package net.psforever.objects.definition.converter package net.psforever.objects.definition.converter
import net.psforever.objects.Player 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} import net.psforever.packet.game.objectcreate.{InventoryItemData, ObjectClass, PlayerData, VehicleData}
object SeatConverter { 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] = { def MakeSeats(seats: Map[Int, Seat], initialOffset: Long): List[InventoryItemData.InventoryItem] = {
var offset = initialOffset var offset = initialOffset
seats seats
.filter({ case (_, seat) => seat.isOccupied }) .filter({ case (_, seat) => seat.isOccupied })
.map({ .map({
case (index, seat) => case (index: Int, seat: Seat) =>
val player = seat.Occupant.get val player = seat.occupant.get
val entry = InventoryItemData(ObjectClass.avatar, player.GUID, index, SeatConverter.MakeSeat(player, offset)) val entry = InventoryItemData(ObjectClass.avatar, player.GUID, index, SeatConverter.MakeSeat(player, offset))
offset += entry.bitsize offset += entry.bitsize
entry entry

View file

@ -14,7 +14,7 @@ class VariantVehicleConverter extends VehicleConverter {
*/ */
Some( Some(
VariantVehicleData( VariantVehicleData(
if (obj.Definition.CanFly && obj.Flying) 7 else 0 if (obj.Definition.CanFly && obj.isFlying) 7 else 0
) )
) )
} }

View file

@ -76,7 +76,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = { private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
val offset: Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) val offset: Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier)
obj.Seats(0).Occupant match { obj.Seats(0).occupant match {
case Some(player) => case Some(player) =>
List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset))) List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset)))
case None => case None =>

View file

@ -13,7 +13,7 @@ object EquipmentSize extends Enumeration {
VehicleWeapon, //vehicle-mounted weapons VehicleWeapon, //vehicle-mounted weapons
BaseTurretWeapon, //common phalanx cannons, and cavern turrets BaseTurretWeapon, //common phalanx cannons, and cavern turrets
BFRArmWeapon, //duel arm weapons for bfr BFRArmWeapon, //duel arm weapons for bfr
BFRGunnerWeapon, //gunner seat for bfr BFRGunnerWeapon, //gunner mount for bfr
Inventory //reserved Inventory //reserved
= Value = Value

View file

@ -118,10 +118,10 @@ trait AggravatedBehavior {
): AggravatedBehavior.Entry = { ): AggravatedBehavior.Entry = {
val cause = data.cause val cause = data.cause
val aggravatedDamageInfo = DamageInteraction( val aggravatedDamageInfo = DamageInteraction(
AggravatedDamage.burning(cause.resolution),
target, target,
data.hitPos,
cause, cause,
data.hitPos AggravatedDamage.burning(cause.resolution)
) )
val entry = AggravatedBehavior.Entry(id, effect, retime, aggravatedDamageInfo, powerOffset) val entry = AggravatedBehavior.Entry(id, effect, retime, aggravatedDamageInfo, powerOffset)
entryIdToEntry += id -> entry entryIdToEntry += id -> entry

View file

@ -36,10 +36,7 @@ object DamageableMountable {
): Unit = { ): Unit = {
val zone = target.Zone val zone = target.Zone
val events = zone.AvatarEvents val events = zone.AvatarEvents
val occupants = target.Seats.values.collect { val occupants = target.Seats.values.toSeq.flatMap { seat => seat.occupants.filter(_.isAlive) }
case seat if seat.isOccupied && seat.Occupant.get.isAlive =>
seat.Occupant.get
}
((cause.adversarial match { ((cause.adversarial match {
case Some(adversarial) => Some(adversarial.attacker) case Some(adversarial) => Some(adversarial.attacker)
case None => None case None => None
@ -80,10 +77,10 @@ object DamageableMountable {
val interaction = cause.interaction val interaction = cause.interaction
target.Seats.values target.Seats.values
.filter(seat => { .filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive seat.isOccupied && seat.occupant.get.isAlive
}) })
.foreach(seat => { .foreach(seat => {
val tplayer = seat.Occupant.get val tplayer = seat.occupant.get
//tplayer.History(cause) //tplayer.History(cause)
tplayer.Actor ! Player.Die( tplayer.Actor ! Player.Die(
DamageInteraction(interaction.resolution, SourceEntry(tplayer), interaction.cause, interaction.hitPos) DamageInteraction(interaction.resolution, SourceEntry(tplayer), interaction.cause, interaction.hitPos)

View file

@ -145,7 +145,7 @@ trait DamageableVehicle
if (aggravated) { if (aggravated) {
val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(totalDamage, Vector3.Zero)) val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(totalDamage, Vector3.Zero))
obj.Seats.values 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 => .foreach { channel =>
events ! VehicleServiceMessage(channel, msg) events ! VehicleServiceMessage(channel, msg)
} }
@ -158,7 +158,7 @@ trait DamageableVehicle
} }
//alert cargo occupants to damage source //alert cargo occupants to damage source
obj.CargoHolds.values.foreach(hold => { obj.CargoHolds.values.foreach(hold => {
hold.Occupant match { hold.occupant match {
case Some(cargo) => case Some(cargo) =>
cargo.Actor ! DamageableVehicle.Damage(cause, totalDamage) cargo.Actor ! DamageableVehicle.Damage(cause, totalDamage)
case None => ; case None => ;
@ -198,7 +198,7 @@ trait DamageableVehicle
DamageableMountable.DestructionAwareness(obj, cause) DamageableMountable.DestructionAwareness(obj, cause)
//cargo vehicles die with us //cargo vehicles die with us
obj.CargoHolds.values.foreach(hold => { obj.CargoHolds.values.foreach(hold => {
hold.Occupant match { hold.occupant match {
case Some(cargo) => case Some(cargo) =>
cargo.Actor ! DamageableVehicle.Destruction(cause) cargo.Actor ! DamageableVehicle.Destruction(cause)
case None => ; case None => ;

View file

@ -73,7 +73,7 @@ trait DamageableWeaponTurret
if (aggravated) { if (aggravated) {
val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(damageToHealth, Vector3.Zero)) val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(damageToHealth, Vector3.Zero))
obj.Seats.values 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 => .foreach { channel =>
events ! VehicleServiceMessage(channel, msg) events ! VehicleServiceMessage(channel, msg)
} }

View file

@ -2,6 +2,7 @@
package net.psforever.objects.serverobject.doors package net.psforever.objects.serverobject.doors
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.packet.game.UseItemMessage import net.psforever.packet.game.UseItemMessage
@ -65,6 +66,14 @@ object Door {
*/ */
final case class NoEvent() extends Exchange 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. * Overloaded constructor.
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields * @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 * @return the `Door` object
*/ */
def Constructor(pos: Vector3)(id: Int, context: ActorContext): Door = { def Constructor(pos: Vector3)(id: Int, context: ActorContext): Door = {
import akka.actor.Props
import net.psforever.objects.GlobalDefinitions 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.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 obj
} }
} }

View file

@ -2,13 +2,12 @@
package net.psforever.objects.serverobject.doors package net.psforever.objects.serverobject.doors
import net.psforever.objects.Player 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.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.locks.IFFLock 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.Service
import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} 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`. * An `Actor` that handles messages being dispatched to a specific `Door`.
@ -18,44 +17,44 @@ class DoorControl(door: Door)
extends PoweredAmenityControl extends PoweredAmenityControl
with FactionAffinityBehavior.Check { with FactionAffinityBehavior.Check {
def FactionObject: FactionAffinity = door def FactionObject: FactionAffinity = door
var isLocked: Boolean = false
var lockingMechanism: Door.LockingMechanismLogic = DoorControl.alwaysOpen
val commonBehavior: Receive = checkBehavior 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 = def poweredStateLogic: Receive =
commonBehavior commonBehavior
.orElse { .orElse {
case CommonMessages.Use(player, _) => case CommonMessages.Use(player, _) =>
val zone = door.Zone if (lockingMechanism(player, door) && !isLocked) {
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
})
) {
openDoor(player) openDoor(player)
} }
case IFFLock.DoorOpenResponse(target: Player) if !isLocked =>
openDoor(target)
case _ => ; case _ => ;
} }
def unpoweredStateLogic: Receive = { def unpoweredStateLogic: Receive = {
commonBehavior commonBehavior
.orElse { .orElse {
case CommonMessages.Use(player, _) => case CommonMessages.Use(player, _) if !isLocked =>
//without power, the door opens freely //without power, the door opens freely
openDoor(player) openDoor(player)
@ -88,3 +87,7 @@ class DoorControl(door: Door)
override def powerTurnOnCallback() : Unit = { } override def powerTurnOnCallback() : Unit = { }
} }
object DoorControl {
def alwaysOpen(obj: PlanetSideServerObject, door: Door): Boolean = true
}

View file

@ -2,9 +2,9 @@
package net.psforever.objects.serverobject.environment package net.psforever.objects.serverobject.environment
import enumeratum.{Enum, EnumEntry} import enumeratum.{Enum, EnumEntry}
import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.vital.Vitality 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, * 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)) 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 { object PieceOfEnvironment {
/** /**
* Did the test point move into or leave the bounds of the represented environment since its previous test? * Did the test point move into or leave the bounds of the represented environment since its previous test?

View file

@ -1,6 +1,9 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.locks 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.hackable.Hackable
import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.packet.game.TriggeredSound import net.psforever.packet.game.TriggeredSound
@ -48,6 +51,14 @@ class IFFLock(private val idef: IFFLockDefinition) extends Amenity with Hackable
} }
object IFFLock { 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. * Overloaded constructor.

View file

@ -6,6 +6,8 @@ import net.psforever.objects.{GlobalDefinitions, SimpleItem}
import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} 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`. * 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}") 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 case _ => ; //no default message
} }
} }

View file

@ -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
}

View file

@ -3,7 +3,8 @@ package net.psforever.objects.serverobject.mount
import akka.actor.ActorRef import akka.actor.ActorRef
import net.psforever.objects.Player 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 * A `Trait` common to all game objects that permit players to
@ -12,38 +13,63 @@ import net.psforever.objects.vehicles.Seat
* @see `Seat` * @see `Seat`
*/ */
trait Mountable { trait Mountable {
protected var seats: Map[Int, Seat] = Map.empty
/** /**
* Retrieve a mapping of each seat from its internal index. * Retrieve a mapping of each mount from its internal index.
* @return the mapping of index to seat * @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. * Given a mount's index position, retrieve the internal `Seat` object.
* @return the specific seat * @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. * Retrieve a mapping of each mount from its mount point index.
* @return the mapping of mount point to seat * @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. * Given a mount point index, return the associated mount index.
* @param mount the mount point * @param mountPoint the mount point
* @return the seat index * @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. * Given a player, determine if that player is seated.
* @param user the player * @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. * 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` * @return the internal `ActorRef`
*/ */
def Actor: ActorRef //TODO can we enforce this desired association to MountableControl? def Actor: ActorRef //TODO can we enforce this desired association to MountableControl?
def Definition: MountableDefinition
} }
object Mountable { object Mountable {
@ -60,10 +88,15 @@ object Mountable {
/** /**
* Message used by the player to indicate the desire to board a `Mountable` object. * Message used by the player to indicate the desire to board a `Mountable` object.
* @param player the player who sent this request message * @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 * @param seat_num the seat index
*/ */
final case class TryMount(player: Player, seat_num: Int)
final case class TryDismount(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. * Message sent in response to the player succeeding to access a `Mountable` object.
* The player should be seated at the given index. * The player should be seated at the given index.
* @param obj the `Mountable` object * @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. * Message sent in response to the player failing to access a `Mountable` object.
* The player would have been be seated at the given index. * The player would have been be seated at the given index.
* @param obj the `Mountable` object * @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. * 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 obj the `Mountable` object
* @param seat_num the seat index * @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. * Message sent in response to the player failing to disembark a `Mountable` object.

View file

@ -2,15 +2,33 @@
package net.psforever.objects.serverobject.mount package net.psforever.objects.serverobject.mount
import akka.actor.Actor import akka.actor.Actor
import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.Player
import net.psforever.objects.entity.{Identifiable, WorldEntity} import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.serverobject.PlanetSideServerObject 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.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. * The logic governing `Mountable` objects that use the `TryMount` message.
@ -18,54 +36,40 @@ object MountableBehavior {
* @see `Seat` * @see `Seat`
* @see `Mountable` * @see `Mountable`
*/ */
trait Mount { val mountBehavior: Receive = {
_: Actor => case Mountable.TryMount(user, mount_point) =>
def MountableObject: PlanetSideServerObject with Mountable with FactionAffinity val obj = MountableObject
obj.GetSeatFromMountPoint(mount_point) match {
val mountBehavior: Receive = { case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) =>
case Mountable.TryMount(user, seat_num) =>
val obj = MountableObject
if (MountTest(MountableObject, seat_num, user)) {
user.VehicleSeated = obj.GUID user.VehicleSeated = obj.GUID
sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) usedMountPoint.put(user.Name, mount_point)
} else { sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point))
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) case _ =>
} sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point))
} }
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
})
}
} }
trait TurretMount extends Mount { protected def mountTest(
_: Actor => 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( private def tryMount(
obj: PlanetSideServerObject with Mountable, obj: PlanetSideServerObject with Mountable,
seatNumber: Int, seatNumber: Int,
player: Player player: Player
): Boolean = { ): Boolean = {
obj match { obj.Seat(seatNumber) match {
case wep: WeaponTurret => case Some(seat) => seat.mount(player).contains(player)
(!wep.Definition.FactionLocked || player.Faction == obj.Faction) && case _ => false
!obj.Destroyed &&
(obj.Seats.get(seatNumber) match {
case Some(seat) => (seat.Occupant = player).contains(player)
case _ => false
})
case _ =>
super.MountTest(obj, seatNumber, player)
}
} }
} }
@ -75,29 +79,41 @@ object MountableBehavior {
* @see `Seat` * @see `Seat`
* @see `Mountable` * @see `Mountable`
*/ */
trait Dismount { val dismountBehavior: Receive = {
this: Actor => 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 = { private def tryDismount(
case Mountable.TryDismount(user, seat_num) => obj: Mountable,
val obj = MountableObject seatNumber: Int,
obj.Seat(seat_num) match { user: Player
case Some(seat) => ): Boolean = {
if ( obj.Seats.get(seatNumber) match {
seat.Bailable || !obj.isMoving(1) || (obj case Some(seat) => seat.unmount(user).isEmpty
.isInstanceOf[Vehicle] && obj.asInstanceOf[Vehicle].DeploymentState == DriveState.Deployed) case _ => false
) {
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))
}
} }
} }
} }

View file

@ -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
}

View file

@ -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]
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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. 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. 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 During this time, a periodic message about the spawn pad being blocked
will be broadcast to all current customers in the order queue. 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 = { def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnControl.Order]): Unit = {
val user = blockedOrder.vehicle val user = blockedOrder.vehicle
.Seats(0) .Seats(0).occupant
.Occupant
.orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner)) .orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner))
.orElse(pad.Zone.GUID(blockedOrder.DriverGUID)) .orElse(pad.Zone.GUID(blockedOrder.DriverGUID))
val relevantRecipients = user match { val relevantRecipients = user match {

View file

@ -75,7 +75,7 @@ object VehicleSpawnPad {
final case class ResetSpawnPad(pad: 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. * This information should only be communicated to the driver's client only.
* @param driver_name the person who will drive the vehicle * @param driver_name the person who will drive the vehicle
* @param vehicle the vehicle being spawned * @param vehicle the vehicle being spawned
@ -84,7 +84,7 @@ object VehicleSpawnPad {
final case class StartPlayerSeatedInVehicle(driver_name: String, vehicle: Vehicle, pad: 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. * This information should only be communicated to the driver's client only.
* @param driver_name the person who will drive the vehicle * @param driver_name the person who will drive the vehicle
* @param vehicle the vehicle being spawned * @param vehicle the vehicle being spawned

View file

@ -14,7 +14,7 @@ import scala.concurrent.duration._
* <br> * <br>
* This object is the first link in the process chain that spawns the ordered vehicle. * 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 * 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. * It has failure cases should the driver be in an incorrect state.
* @param pad the `VehicleSpawnPad` object being governed * @param pad the `VehicleSpawnPad` object being governed
*/ */

View file

@ -23,7 +23,7 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont
def LogId = "-lifter" def LogId = "-lifter"
val seatDriver = 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 = { def receive: Receive = {
case order @ VehicleSpawnControl.Order(_, vehicle) => case order @ VehicleSpawnControl.Order(_, vehicle) =>

View file

@ -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. * 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.<br> * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.<br>
* <br> * <br>
* 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. * 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. * 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. * 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 seat. * 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. * It has failure cases should the driver or the vehicle be in an incorrect state.
* @see `ZonePopulationActor` * @see `ZonePopulationActor`
* @param pad the `VehicleSpawnPad` object being governed * @param pad the `VehicleSpawnPad` object being governed

View file

@ -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.<br>
* <br>
* 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
}
}
}

View file

@ -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.<br>
* <br>
* 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
}
}

View file

@ -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`.<br>
* <br>
* 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
}
}

View file

@ -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
}
}

View file

@ -21,11 +21,12 @@ trait CaptureTerminalAwareBehavior {
if (CaptureTerminalAwareObject.isInstanceOf[Mountable]) { if (CaptureTerminalAwareObject.isInstanceOf[Mountable]) {
CaptureTerminalAwareObject.asInstanceOf[Mountable].Seats.filter(x => x._2.isOccupied).foreach(x => { CaptureTerminalAwareObject.asInstanceOf[Mountable].Seats.filter(x => x._2.isOccupied).foreach(x => {
val (seat_num, seat) = x val (seat_num, seat) = x
val user = seat.occupant.get
CaptureTerminalAwareObject.Zone.VehicleEvents ! VehicleServiceMessage( CaptureTerminalAwareObject.Zone.VehicleEvents ! VehicleServiceMessage(
CaptureTerminalAwareObject.Zone.id, CaptureTerminalAwareObject.Zone.id,
VehicleAction.KickPassenger(seat.Occupant.get.GUID, seat_num, true, CaptureTerminalAwareObject.GUID)) VehicleAction.KickPassenger(user.GUID, seat_num, true, CaptureTerminalAwareObject.GUID)
)
seat.Occupant = None seat.unmount(user)
}) })
} }
} }

View file

@ -1,14 +1,9 @@
package net.psforever.objects.serverobject.terminals.capture package net.psforever.objects.serverobject.terminals.capture
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.serverobject.CommonMessages 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.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.PlanetSideEmpire
import java.util.concurrent.TimeUnit
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
object CaptureTerminals { object CaptureTerminals {

View file

@ -1,12 +1,10 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.terminals.implant package net.psforever.objects.serverobject.terminals.implant
import net.psforever.objects.Player
import net.psforever.objects.serverobject.hackable.Hackable 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.structures.Amenity
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
import net.psforever.objects.vehicles.Seat
import net.psforever.packet.game.TriggeredSound import net.psforever.packet.game.TriggeredSound
import net.psforever.types.Vector3 import net.psforever.types.Vector3
@ -20,28 +18,12 @@ class ImplantTerminalMech(private val idef: ImplantTerminalMechDefinition)
with Mountable with Mountable
with Hackable with Hackable
with CaptureTerminalAware { 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 HackSound = TriggeredSound.HackTerminal
HackEffectDuration = Array(0, 30, 60, 90) HackEffectDuration = Array(0, 30, 60, 90)
HackDuration = Array(0, 10, 5, 3) 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 def Definition: ImplantTerminalMechDefinition = idef
} }

View file

@ -21,8 +21,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
class ImplantTerminalMechControl(mech: ImplantTerminalMech) class ImplantTerminalMechControl(mech: ImplantTerminalMech)
extends PoweredAmenityControl extends PoweredAmenityControl
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with MountableBehavior.Mount with MountableBehavior
with MountableBehavior.Dismount
with HackableBehavior.GenericHackable with HackableBehavior.GenericHackable
with DamageableEntity with DamageableEntity
with RepairableEntity with RepairableEntity
@ -68,11 +67,11 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
case _ => ; case _ => ;
} }
override protected def MountTest( override protected def mountTest(
obj: PlanetSideServerObject with Mountable, obj: PlanetSideServerObject with Mountable,
seatNumber: Int, seatNumber: Int,
player: Player player: Player
): Boolean = { ): Boolean = {
val zone = obj.Zone val zone = obj.Zone
zone.map.terminalToInterface.get(obj.GUID.guid) match { zone.map.terminalToInterface.get(obj.GUID.guid) match {
case Some(interface_guid) => case Some(interface_guid) =>
@ -80,7 +79,7 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
case Some(interface) => !interface.Destroyed case Some(interface) => !interface.Destroyed
case None => false case None => false
}) && }) &&
super.MountTest(obj, seatNumber, player) super.mountTest(obj, seatNumber, player)
case None => case None =>
false false
} }
@ -122,9 +121,9 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
val zoneId = zone.id val zoneId = zone.id
val events = zone.VehicleEvents val events = zone.VehicleEvents
mech.Seats.values.foreach(seat => mech.Seats.values.foreach(seat =>
seat.Occupant match { seat.occupant match {
case Some(player) => case Some(player) =>
seat.Occupant = None seat.unmount(player)
player.VehicleSeated = None player.VehicleSeated = None
if (player.HasGUID) { if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid))

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.terminals.implant 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 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. * Implant terminals are composed of two components.
* This `Definition` constructs the visible mechanical tube component that can be mounted. * This `Definition` constructs the visible mechanical tube component that can be mounted.
*/ */
class ImplantTerminalMechDefinition extends AmenityDefinition(410) { class ImplantTerminalMechDefinition
/* key - seat index, value - seat object */ extends AmenityDefinition(410)
private val seats: Map[Int, SeatDefinition] = Map(0 -> new SeatDefinition) with MountableDefinition {
/* key - entry point index, value - seat index */
private val mountPoints: Map[Int, Int] = Map(1 -> 0)
Name = "implant_terminal_mech" Name = "implant_terminal_mech"
def Seats: Map[Int, SeatDefinition] = seats /* key - mount index, value - mount object */
Seats += 0 -> new SeatDefinition() {
def MountPoints: Map[Int, Int] = mountPoints restriction = Unrestricted
}
/* key - entry point index, value - mount index */
MountPoints += 1 -> MountInfo(0)
} }

View file

@ -13,8 +13,6 @@ class FacilityTurret(tDef: FacilityTurretDefinition)
with CaptureTerminalAware { with CaptureTerminalAware {
WeaponTurret.LoadDefinition(this) WeaponTurret.LoadDefinition(this)
def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap
def Definition: FacilityTurretDefinition = tDef def Definition: FacilityTurretDefinition = tDef
} }

View file

@ -3,8 +3,8 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool} import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons} import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons}
import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.mount.MountableBehavior import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret} import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret}
import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.hackable.GenericHackables
@ -31,8 +31,7 @@ import scala.concurrent.duration._
class FacilityTurretControl(turret: FacilityTurret) class FacilityTurretControl(turret: FacilityTurret)
extends PoweredAmenityControl extends PoweredAmenityControl
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with MountableBehavior.TurretMount with MountableBehavior
with MountableBehavior.Dismount
with DamageableWeaponTurret with DamageableWeaponTurret
with RepairableWeaponTurret with RepairableWeaponTurret
with AmenityAutoRepair with AmenityAutoRepair
@ -74,7 +73,7 @@ class FacilityTurretControl(turret: FacilityTurret)
item.Magazine > 0 && turret.Seats.values.forall(!_.isOccupied) => item.Magazine > 0 && turret.Seats.values.forall(!_.isOccupied) =>
TurretUpgrade.values.find(_.id == upgradeValue) match { TurretUpgrade.values.find(_.id == upgradeValue) match {
case Some(upgrade) case Some(upgrade)
if turret.Upgrade != upgrade && turret.Definition.Weapons.values if turret.Upgrade != upgrade && turret.Definition.WeaponPaths.values
.flatMap(_.keySet) .flatMap(_.keySet)
.exists(_ == upgrade) => .exists(_ == upgrade) =>
sender() ! CommonMessages.Progress( sender() ! CommonMessages.Progress(
@ -103,7 +102,7 @@ class FacilityTurretControl(turret: FacilityTurret)
if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) {
weapon.Magazine += 1 weapon.Magazine += 1
val seat = turret.Seat(0).get val seat = turret.Seat(0).get
seat.Occupant match { seat.occupant match {
case Some(player: Player) => case Some(player: Player) =>
turret.Zone.LocalEvents ! LocalServiceMessage( turret.Zone.LocalEvents ! LocalServiceMessage(
turret.Zone.id, turret.Zone.id,
@ -126,6 +125,13 @@ class FacilityTurretControl(turret: FacilityTurret)
case _ => ; 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 = { override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = {
tryAutoRepair() tryAutoRepair()
super.DamageAwareness(target, cause, amount) super.DamageAwareness(target, cause, amount)
@ -172,9 +178,9 @@ class FacilityTurretControl(turret: FacilityTurret)
val zoneId = zone.id val zoneId = zone.id
val events = zone.VehicleEvents val events = zone.VehicleEvents
turret.Seats.values.foreach(seat => turret.Seats.values.foreach(seat =>
seat.Occupant match { seat.occupant match {
case Some(player) => case Some(player) =>
seat.Occupant = None seat.unmount(player)
player.VehicleSeated = None player.VehicleSeated = None
if (player.HasGUID) { if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid))

View file

@ -9,7 +9,9 @@ import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance
* The definition for any `FacilityTurret`. * The definition for any `FacilityTurret`.
* @param objectId the object's identifier number * @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 DamageUsing = DamageCalculations.AgainstVehicle
ResistUsing = StandardVehicleResistance ResistUsing = StandardVehicleResistance
Model = SimpleResolutions.calculate Model = SimpleResolutions.calculate

View file

@ -2,7 +2,7 @@
package net.psforever.objects.serverobject.turret package net.psforever.objects.serverobject.turret
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition} 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.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -11,14 +11,14 @@ import scala.collection.mutable
/** /**
* The definition for any `MannedTurret`. * The definition for any `MannedTurret`.
*/ */
trait TurretDefinition extends ResistanceProfileMutators with DamageResistanceModel { trait TurretDefinition
extends MountableWeaponsDefinition
with ResistanceProfileMutators
with DamageResistanceModel {
odef: ObjectDefinition => odef: ObjectDefinition =>
Turrets(odef.ObjectId) //let throw NoSuchElementException 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 */ /* 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]]() mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]]()
/** can only be mounted by owning faction when `true` */ /** can only be mounted by owning faction when `true` */
@ -29,9 +29,7 @@ trait TurretDefinition extends ResistanceProfileMutators with DamageResistanceMo
*/ */
private var hasReserveAmmunition: Boolean = false private var hasReserveAmmunition: Boolean = false
def MountPoints: mutable.HashMap[Int, Int] = mountPoints def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths
def Weapons: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weapons
def FactionLocked: Boolean = factionLocked def FactionLocked: Boolean = factionLocked

View file

@ -1,22 +1,22 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret package net.psforever.objects.serverobject.turret
import net.psforever.objects.{AmmoBox, PlanetSideGameObject, Player, Tool} import net.psforever.objects.{AmmoBox, PlanetSideGameObject, Tool}
import net.psforever.objects.definition.{AmmoBoxDefinition, SeatDefinition, ToolDefinition} import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSlot} import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.inventory.{Container, GridInventory} import net.psforever.objects.inventory.{Container, GridInventory}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.{SeatDefinition, Seat => Chair}
import net.psforever.objects.vehicles.{MountedWeapons, 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 => _: PlanetSideGameObject =>
/** manned turrets have just one seat; this is just standard interface */ /** manned turrets have just one mount; this is just standard interface */
protected val seats: Map[Int, Chair] = Map(0 -> Chair(new SeatDefinition() { ControlledWeapon = Some(1) })) seats = Map(0 -> new Chair(new SeatDefinition()))
/** turrets have just one weapon; this is just standard interface */
protected var weapons: Map[Int, EquipmentSlot] = Map.empty
/** may or may not have inaccessible inventory space /** may or may not have inaccessible inventory space
* see `ReserveAmmunition` in the definition * 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 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: TurretUpgrade.Value = upgradePath
def Upgrade_=(upgrade: TurretUpgrade.Value): TurretUpgrade.Value = { 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 //upgrade each weapon as long as that weapon has a valid option for that upgrade
Definition match { Definition match {
case definition: TurretDefinition => case definition: TurretDefinition =>
definition.Weapons.foreach({ definition.WeaponPaths.foreach({
case (index, upgradePaths) => case (index, upgradePaths) =>
if (upgradePaths.contains(upgrade)) { if (upgradePaths.contains(upgrade)) {
updated = true updated = true
@ -136,7 +103,7 @@ object WeaponTurret {
def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = { def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = {
import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon
//create weapons; note the class //create weapons; note the class
turret.weapons = tdef.Weapons turret.weapons = tdef.WeaponPaths
.map({ .map({
case (num, upgradePaths) => case (num, upgradePaths) =>
val slot = EquipmentSlot(BaseTurretWeapon) val slot = EquipmentSlot(BaseTurretWeapon)
@ -146,7 +113,7 @@ object WeaponTurret {
.toMap .toMap
//special inventory ammunition object(s) //special inventory ammunition object(s)
if (tdef.ReserveAmmunition) { 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) { if (allAmmunitionTypes.nonEmpty) {
turret.inventory.Resize(allAmmunitionTypes.size, 1) turret.inventory.Resize(allAmmunitionTypes.size, 1)
var i: Int = 0 var i: Int = 0

View file

@ -3,9 +3,9 @@ package net.psforever.objects.vehicles
/** /**
* An `Enumeration` of various permission groups that control access to aspects of a vehicle.<br> * An `Enumeration` of various permission groups that control access to aspects of a vehicle.<br>
* - `Driver` is a seat that is always seat number 0.<br> * - `Driver` is a mount that is always mount number 0.<br>
* - `Gunner` is a seat that is not the `Driver` and controls a mounted weapon.<br> * - `Gunner` is a mount that is not the `Driver` and controls a mounted weapon.<br>
* - `Passenger` is a seat that is not the `Driver` and does not have control of a mounted weapon.<br> * - `Passenger` is a mount that is not the `Driver` and does not have control of a mounted weapon.<br>
* - `Trunk` represnts access to the vehicle's internal storage space.<br> * - `Trunk` represnts access to the vehicle's internal storage space.<br>
* Organized to replicate the `PlanetsideAttributeMessage` value used for that given access level. * 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. * In their respective `PlanetsideAttributeMessage` packet, the groups are indexed in the same order as 10 through 13.

View file

@ -1,88 +1,11 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles package net.psforever.objects.vehicles
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.objects.definition.{CargoDefinition} import net.psforever.objects.serverobject.mount.{MountableSpace, MountableSpaceDefinition}
/** class Cargo(private val cdef: MountableSpaceDefinition[Vehicle]) extends MountableSpace[Vehicle] {
* Server-side support for a slot that vehicles can occupy override protected def testToMount(target: Vehicle): Boolean = target.MountedIn.isEmpty && super.testToMount(target)
* @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
/** def definition: MountableSpaceDefinition[Vehicle] = cdef
* 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"
}
} }

View file

@ -149,7 +149,7 @@ object CargoBehavior {
//cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it //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") log.info(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance")
cargo.MountedIn = carrierGUID cargo.MountedIn = carrierGUID
hold.Occupant = cargo hold.mount(cargo)
cargo.Velocity = None cargo.Velocity = None
zone.VehicleEvents ! VehicleServiceMessage( zone.VehicleEvents ! VehicleServiceMessage(
s"${cargo.Actor}", s"${cargo.Actor}",
@ -168,7 +168,7 @@ object CargoBehavior {
log.info( log.info(
"HandleCheckCargoMounting: cargo vehicle is too far away or didn't mount within allocated time - aborting" "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.VehicleEvents ! VehicleServiceMessage(
zone.id, zone.id,
VehicleAction.SendResponse( VehicleAction.SendResponse(
@ -266,7 +266,7 @@ object CargoBehavior {
log.info( log.info(
s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance" 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.VehicleEvents ! VehicleServiceMessage(
zone.id, zone.id,
VehicleAction.SendResponse( VehicleAction.SendResponse(
@ -289,7 +289,7 @@ object CargoBehavior {
} else if (iteration > 40) { } 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 vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold
cargo.MountedIn = carrierGUID cargo.MountedIn = carrierGUID
hold.Occupant = cargo hold.mount(cargo)
CargoMountBehaviorForAll(carrier, cargo, mountPoint) CargoMountBehaviorForAll(carrier, cargo, mountPoint)
false false
} else { } else {
@ -363,11 +363,11 @@ object CargoBehavior {
kicked: Boolean kicked: Boolean
): Unit = { ): Unit = {
val zone = carrier.Zone 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)) => case Some((mountPoint, hold)) =>
cargo.MountedIn = None cargo.MountedIn = None
hold.Occupant = None hold.unmount(cargo)
val driverOpt = cargo.Seats(0).Occupant val driverOpt = cargo.Seats(0).occupant
val rotation: Vector3 = if (Vehicles.CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set val rotation: Vector3 = if (Vehicles.CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set
//dismount router "sideways" in a lodestar //dismount router "sideways" in a lodestar
carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360) carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360)
@ -393,7 +393,7 @@ object CargoBehavior {
s"$cargoActor", s"$cargoActor",
VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))
) )
if (carrier.Flying) { if (carrier.isFlying) {
//the carrier vehicle is flying; eject the cargo vehicle //the carrier vehicle is flying; eject the cargo vehicle
val ejectCargoMsg = val ejectCargoMsg =
CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.InProgress, 0) CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.InProgress, 0)

View file

@ -2,11 +2,11 @@
package net.psforever.objects.vehicles package net.psforever.objects.vehicles
/** /**
* An `Enumeration` of exo-suit-based seat access restrictions.<br> * An `Enumeration` of exo-suit-based mount access restrictions.<br>
* <br> * <br>
* 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. * `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 { object CargoVehicleRestriction extends Enumeration {
type Type = Value type Type = Value

View file

@ -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
}

View file

@ -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]()
}

View file

@ -2,38 +2,13 @@
package net.psforever.objects.vehicles package net.psforever.objects.vehicles
import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.equipment.{Equipment, EquipmentSlot} import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles.{Seat => Chair}
trait MountedWeapons { 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
/** def Definition: MountedWeaponsDefinition
* 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]
} }

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -2,14 +2,15 @@
package net.psforever.objects.vehicles package net.psforever.objects.vehicles
/** /**
* An `Enumeration` of exo-suit-based seat access restrictions.<br> * An `Enumeration` of exo-suit-based mount access restrictions.<br>
* <br> * <br>
* 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. * `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 { object SeatArmorRestriction extends Enumeration {
type Type = Value type Type = Value
val MaxOnly, NoMax, NoReinforcedOrMax = Value val MaxOnly, NoMax, NoReinforcedOrMax, Unrestricted = Value
} }

View file

@ -5,6 +5,7 @@ import akka.actor.{Actor, Cancellable}
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.ballistics.VehicleSource import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.ce.TelepadLike import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons} import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons}
import net.psforever.objects.guid.GUIDTask import net.psforever.objects.guid.GUIDTask
import net.psforever.objects.inventory.{GridInventory, InventoryItem} import net.psforever.objects.inventory.{GridInventory, InventoryItem}
@ -48,8 +49,7 @@ class VehicleControl(vehicle: Vehicle)
extends Actor extends Actor
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with DeploymentBehavior with DeploymentBehavior
with MountableBehavior.Mount with MountableBehavior
with MountableBehavior.Dismount
with CargoBehavior with CargoBehavior
with DamageableVehicle with DamageableVehicle
with RepairableVehicle with RepairableVehicle
@ -129,34 +129,13 @@ class VehicleControl(vehicle: Vehicle)
case Vehicle.Ownership(Some(player)) => case Vehicle.Ownership(Some(player)) =>
GainOwnership(player) GainOwnership(player)
case msg@Mountable.TryMount(player, seat_num) => case msg @ Mountable.TryMount(player, mount_point) =>
tryMountBehavior.apply(msg) mountBehavior.apply(msg)
val obj = MountableObject mountCleanup(mount_point, player)
//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.TryDismount => case msg @ Mountable.TryDismount(_, seat_num) =>
dismountBehavior.apply(msg) dismountBehavior.apply(msg)
dismountCleanup() dismountCleanup(seat_num)
case Vehicle.ChargeShields(amount) => case Vehicle.ChargeShields(amount) =>
val now : Long = System.currentTimeMillis() val now : Long = System.currentTimeMillis()
@ -261,7 +240,7 @@ class VehicleControl(vehicle: Vehicle)
case Vehicle.Deconstruct(time) => case Vehicle.Deconstruct(time) =>
time match { time match {
case Some(delay) => case Some(delay) if vehicle.Definition.undergoesDecay =>
decaying = true decaying = true
decayTimer.cancel() decayTimer.cancel()
decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion())
@ -288,13 +267,13 @@ class VehicleControl(vehicle: Vehicle)
case msg : Deployment.TryUndeploy => case msg : Deployment.TryUndeploy =>
deployBehavior.apply(msg) deployBehavior.apply(msg)
case msg : Mountable.TryDismount => case msg @ Mountable.TryDismount(_, seat_num) =>
dismountBehavior.apply(msg) dismountBehavior.apply(msg)
dismountCleanup() dismountCleanup(seat_num)
case Vehicle.Deconstruct(time) => case Vehicle.Deconstruct(time) =>
time match { time match {
case Some(delay) => case Some(delay) if vehicle.Definition.undergoesDecay =>
decaying = true decaying = true
decayTimer.cancel() decayTimer.cancel()
decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion())
@ -327,46 +306,77 @@ class VehicleControl(vehicle: Vehicle)
case _ => case _ =>
} }
val tryMountBehavior : Receive = { override protected def mountTest(
case msg @ Mountable.TryMount(user, seat_num) => obj: PlanetSideServerObject with Mountable,
val exosuit = user.ExoSuit seatNumber: Int,
val restriction = vehicle.Seats(seat_num).ArmorRestriction user: Player
val seatGroup = vehicle.SeatPermissionGroup(seat_num).getOrElse(AccessPermissionGroup.Passenger) ): Boolean = {
val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger)
if ( val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire)
(if (seatGroup == AccessPermissionGroup.Driver) { (if (seatGroup == AccessPermissionGroup.Driver) {
vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked
} } else {
else { permission != VehicleLockState.Locked
permission != VehicleLockState.Locked }) &&
}) && super.mountTest(obj, seatNumber, user)
(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))
}
} }
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 val obj = MountableObject
// Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
if (!obj.Seats(0).isOccupied) { if (!obj.Seats(0).isOccupied) {
obj.Velocity = Some(Vector3.Zero) obj.Velocity = Some(Vector3.Zero)
} }
//are we already decaying? are we unowned? is no one seated anywhere? if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated
if (!decaying && obj.Owner.isEmpty && obj.Seats.values.forall(!_.isOccupied)) { //we were only owning the vehicle while we sat in its driver seat
decaying = true val canBeOwned = obj.Definition.CanBeOwned
decayTimer = context.system.scheduler.scheduleOnce( if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), LoseOwnership()
self, }
VehicleControl.PrepareForDeletion() //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 _ => ; case _ => ;
} }
if (!vehicle.Flying || kickPassengers) { if (!vehicle.isFlying || kickPassengers) {
//kick all passengers (either not flying, or being explicitly instructed) //kick all passengers (either not flying, or being explicitly instructed)
vehicle.Seats.values.foreach { seat => vehicle.Seats.values.foreach { seat =>
seat.Occupant match { seat.occupant match {
case Some(player) => case Some(player) =>
seat.Occupant = None seat.unmount(player)
player.VehicleSeated = None player.VehicleSeated = None
if (player.HasGUID) { if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid))
@ -408,7 +418,7 @@ class VehicleControl(vehicle: Vehicle)
vehicle.CargoHolds.values vehicle.CargoHolds.values
.collect { .collect {
case hold if hold.isOccupied => case hold if hold.isOccupied =>
val cargo = hold.Occupant.get val cargo = hold.occupant.get
CargoBehavior.HandleVehicleCargoDismount( CargoBehavior.HandleVehicleCargoDismount(
cargo.GUID, cargo.GUID,
cargo, cargo,
@ -423,10 +433,7 @@ class VehicleControl(vehicle: Vehicle)
def PrepareForDeletion() : Unit = { def PrepareForDeletion() : Unit = {
decaying = false decaying = false
val guid = vehicle.GUID
val zone = vehicle.Zone val zone = vehicle.Zone
val zoneId = zone.id
val events = zone.VehicleEvents
//miscellaneous changes //miscellaneous changes
Vehicles.BeforeUnloadVehicle(vehicle, zone) Vehicles.BeforeUnloadVehicle(vehicle, zone)
//cancel jammed behavior //cancel jammed behavior
@ -449,7 +456,10 @@ class VehicleControl(vehicle: Vehicle)
def LoseOwnership(): Unit = { def LoseOwnership(): Unit = {
val obj = MountableObject val obj = MountableObject
Vehicles.Disown(obj.GUID, obj) 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 decaying = true
decayTimer = context.system.scheduler.scheduleOnce( decayTimer = context.system.scheduler.scheduleOnce(
obj.Definition.DeconstructionTime.getOrElse(5 minutes), obj.Definition.DeconstructionTime.getOrElse(5 minutes),
@ -460,7 +470,9 @@ class VehicleControl(vehicle: Vehicle)
} }
def GainOwnership(player: Player): Unit = { 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(_) => case Some(_) =>
decaying = false decaying = false
decayTimer.cancel() decayTimer.cancel()
@ -538,7 +550,7 @@ class VehicleControl(vehicle: Vehicle)
val toChannel = if (obj.VisibleSlots.contains(fromSlot)) zone.id else self.toString val toChannel = if (obj.VisibleSlots.contains(fromSlot)) zone.id else self.toString
zone.VehicleEvents ! VehicleServiceMessage( zone.VehicleEvents ! VehicleServiceMessage(
toChannel, toChannel,
VehicleAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID) VehicleAction.ObjectDelete(item.GUID)
) )
} }
@ -558,7 +570,7 @@ class VehicleControl(vehicle: Vehicle)
val zone = vehicle.Zone val zone = vehicle.Zone
val zoneChannel = zone.id val zoneChannel = zone.id
val GUID0 = Service.defaultPlayerGUID val GUID0 = Service.defaultPlayerGUID
val driverChannel = vehicle.Seats(0).Occupant match { val driverChannel = vehicle.Seats(0).occupant match {
case Some(tplayer) => tplayer.Name case Some(tplayer) => tplayer.Name
case None => "" case None => ""
} }
@ -623,7 +635,7 @@ class VehicleControl(vehicle: Vehicle)
val guid = vehicle.GUID val guid = vehicle.GUID
val zone = vehicle.Zone val zone = vehicle.Zone
val GUID0 = Service.defaultPlayerGUID val GUID0 = Service.defaultPlayerGUID
val driverChannel = vehicle.Seats(0).Occupant match { val driverChannel = vehicle.Seats(0).occupant match {
case Some(tplayer) => tplayer.Name case Some(tplayer) => tplayer.Name
case None => "" case None => ""
} }
@ -686,7 +698,7 @@ class VehicleControl(vehicle: Vehicle)
percentage, percentage,
body, body,
vehicle.Seats.values 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) } .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) }
) )
} }
@ -779,7 +791,7 @@ class VehicleControl(vehicle: Vehicle)
percentage, percentage,
body, body,
vehicle.Seats.values 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) } .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) }
) )
} }

View file

@ -2,6 +2,7 @@
package net.psforever.objects.vehicles package net.psforever.objects.vehicles
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.objects.serverobject.mount.Seat
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
/** /**
@ -14,7 +15,7 @@ import net.psforever.objects.zones.Zone
* @param vehicle the vehicle in transport * @param vehicle the vehicle in transport
* @param origin where the vehicle originally was * @param origin where the vehicle originally was
* @param driverName the name of the driver when the transport process started * @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 * @param cargo the paired driver names and cargo hold indices of all cargo vehicles when the transport process started
*/ */
final case class VehicleManifest( final case class VehicleManifest(
@ -28,17 +29,17 @@ final case class VehicleManifest(
object VehicleManifest { object VehicleManifest {
def apply(vehicle: Vehicle): 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 Some(driver) => driver.Name
case None => "MISSING_DRIVER" case None => "MISSING_DRIVER"
} }
val passengers = vehicle.Seats.collect { val passengers = vehicle.Seats.collect {
case (index, seat) if index > 0 && seat.isOccupied => case (index: Int, seat: Seat) if index > 0 && seat.isOccupied =>
(seat.Occupant.get.Name, index) (seat.occupant.get.Name, index)
} }
val cargo = vehicle.CargoHolds.collect { val cargo = vehicle.CargoHolds.collect {
case (index, hold) if hold.Occupant.nonEmpty => case (index: Int, hold: Cargo) if hold.occupant.nonEmpty =>
hold.Occupant.get.Seats(0).Occupant match { hold.occupant.get.Seats(0).occupant match {
case Some(driver) => case Some(driver) =>
(driver.Name, index) (driver.Name, index)
case None => case None =>

View file

@ -392,7 +392,7 @@ object ProjectileDamageModifierFunctions {
data: DamageInteraction, data: DamageInteraction,
cause: ProjectileReason cause: ProjectileReason
): Int = { ): Int = {
if (cause.resolution == resolution) { if (data.resolution == resolution) {
(data.cause.source.Aggravated, data.target) match { (data.cause.source.Aggravated, data.target) match {
case (Some(aggravation), p: PlayerSource) => case (Some(aggravation), p: PlayerSource) =>
val degradation = aggravation.info.find(_.damage_type == damageType) match { val degradation = aggravation.info.find(_.damage_type == damageType) match {

View file

@ -2,7 +2,7 @@ package net.psforever.objects.zones
import enumeratum.values.{StringEnum, StringEnumEntry} import enumeratum.values.{StringEnum, StringEnumEntry}
import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.environment._
import net.psforever.types.Vector3 import net.psforever.types.{PlanetSideGUID, Vector3}
sealed abstract class MapInfo( sealed abstract class MapInfo(
val value: String, 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.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, 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 Pool(EnvironmentAttribute.Water, 31.03125f, 4849.797f, 2415.4297f, 4731.8594f, 2252.1484f) //south of hart c campus
) ) ++ MapEnvironment.map11Environment
) )
case object Map12 case object Map12
@ -188,7 +188,8 @@ case object MapInfo extends StringEnum[MapInfo] {
value = "map12", value = "map12",
checksum = 962888126L, checksum = 962888126L,
scale = MapScale.Dim8192, scale = MapScale.Dim8192,
environment = List(SeaLevel(EnvironmentAttribute.Water, 20.03125f)) environment = List(SeaLevel(EnvironmentAttribute.Water, 20.03125f)) ++
MapEnvironment.map12Environment
) )
case object Map13 case object Map13
@ -196,7 +197,8 @@ case object MapInfo extends StringEnum[MapInfo] {
value = "map13", value = "map13",
checksum = 3904659548L, checksum = 3904659548L,
scale = MapScale.Dim8192, scale = MapScale.Dim8192,
environment = List(SeaLevel(EnvironmentAttribute.Water, 30)) environment = List(SeaLevel(EnvironmentAttribute.Water, 30)) ++
MapEnvironment.map13Environment
) )
case object Map14 case object Map14
@ -317,3 +319,107 @@ case object MapInfo extends StringEnum[MapInfo] {
val values: IndexedSeq[MapInfo] = findValues 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.<br>
* <br>
* 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.<br>
* <br>
* 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
)
}
}

View file

@ -40,13 +40,17 @@ import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.Avatar
import net.psforever.objects.geometry.Geometry3D import net.psforever.objects.geometry.Geometry3D
import net.psforever.objects.serverobject.PlanetSideServerObject 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.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.vehicles.UtilityType import net.psforever.objects.vehicles.UtilityType
import net.psforever.objects.vital.etc.{EmpReason, ExplodingEntityReason} 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.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.prop.DamageWithPosition 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.<br> * A server object representing the one-landmass planets as well as the individual subterranean caverns.<br>
@ -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 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) //ntu management (eventually move to a generic building startup function)
buildings.values buildings.values
.flatMap(_.Amenities.filter(_.Definition == GlobalDefinitions.resource_silo)) .flatMap(_.Amenities.filter(_.Definition == GlobalDefinitions.resource_silo))
@ -661,6 +672,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
case painbox: Painbox => case painbox: Painbox =>
painbox.Actor ! "startup" 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 //allocate soi information
soi ! SOI.Build() soi ! SOI.Build()
} }
@ -924,6 +941,10 @@ object Zone {
final case class Despawn(vehicle: Vehicle) 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 CanNotSpawn(zone: Zone, vehicle: Vehicle, reason: String)
final case class CanNotDespawn(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 = { def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = {
distanceCheck(obj1.Definition.Geometry(obj1), obj2.Definition.Geometry(obj2), maxDistance) 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. * 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 * @param g1 the geometric representation of a game entity

View file

@ -33,12 +33,13 @@ class ZoneMap(val name: String) {
var checksum: Long = 0 var checksum: Long = 0
var zipLinePaths: List[ZipLinePath] = List() var zipLinePaths: List[ZipLinePath] = List()
var cavern: Boolean = false var cavern: Boolean = false
var environment: List[PieceOfEnvironment] = List() var environment: List[PieceOfEnvironment] = List()
private var linkTurretWeapon: Map[Int, Int] = Map() private var linkTurretWeapon: Map[Int, Int] = Map()
private var linkTerminalPad: Map[Int, Int] = Map() private var linkTerminalPad: Map[Int, Int] = Map()
private var linkTerminalInterface: Map[Int, Int] = Map() private var linkTerminalInterface: Map[Int, Int] = Map()
private var linkDoorLock: Map[Int, Int] = Map() private var linkDoorLock: Map[Int, Int] = Map()
private var linkObjectBase: 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 buildings: Map[(String, Int, Int), FoundationBuilder] = Map()
private var lattice: Set[(String, String)] = Set() private var lattice: Set[(String, String)] = Set()
@ -116,6 +117,12 @@ class ZoneMap(val name: String) {
linkTurretWeapon = linkTurretWeapon ++ Map(turretGuid -> weaponGuid) 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 latticeLink: Set[(String, String)] = lattice
def addLatticeLink(source: String, target: String): Unit = { def addLatticeLink(source: String, target: String): Unit = {

View file

@ -44,6 +44,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
vehicle.Actor = vehicle.Actor =
context.actorOf(Props(classOf[VehicleControl], vehicle), PlanetSideServerObject.UniqueActorName(vehicle)) context.actorOf(Props(classOf[VehicleControl], vehicle), PlanetSideServerObject.UniqueActorName(vehicle))
} }
sender() ! Zone.Vehicle.HasSpawned(zone, vehicle)
case Zone.Vehicle.Despawn(vehicle) => case Zone.Vehicle.Despawn(vehicle) =>
ZoneVehicleActor.recursiveFindVehicle(vehicleList.iterator, vehicle) match { ZoneVehicleActor.recursiveFindVehicle(vehicleList.iterator, vehicle) match {
@ -51,6 +52,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
vehicleList.remove(index) vehicleList.remove(index)
context.stop(vehicle.Actor) context.stop(vehicle.Actor)
vehicle.Actor = Default.Actor vehicle.Actor = Default.Actor
sender() ! Zone.Vehicle.HasDespawned(zone, vehicle)
case None => ; case None => ;
sender() ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find") sender() ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find")
} }

View file

@ -401,16 +401,16 @@ object GamePacketOpcode extends Enumeration {
case 0x50 => game.TargetingInfoMessage.decode case 0x50 => game.TargetingInfoMessage.decode
case 0x51 => game.TriggerEffectMessage.decode case 0x51 => game.TriggerEffectMessage.decode
case 0x52 => game.WeaponDryFireMessage.decode case 0x52 => game.WeaponDryFireMessage.decode
case 0x53 => noDecoder(DroppodLaunchRequestMessage) case 0x53 => game.DroppodLaunchRequestMessage.decode
case 0x54 => game.HackMessage.decode case 0x54 => game.HackMessage.decode
case 0x55 => noDecoder(DroppodLaunchResponseMessage) case 0x55 => game.DroppodLaunchResponseMessage.decode
case 0x56 => game.GenericObjectActionMessage.decode case 0x56 => game.GenericObjectActionMessage.decode
case 0x57 => game.AvatarVehicleTimerMessage.decode case 0x57 => game.AvatarVehicleTimerMessage.decode
// 0x58 // 0x58
case 0x58 => game.AvatarImplantMessage.decode case 0x58 => game.AvatarImplantMessage.decode
case 0x59 => noDecoder(UnknownMessage89) case 0x59 => noDecoder(UnknownMessage89)
case 0x5a => game.DelayedPathMountMsg.decode case 0x5a => game.DelayedPathMountMsg.decode
case 0x5b => noDecoder(OrbitalShuttleTimeMsg) case 0x5b => game.OrbitalShuttleTimeMsg.decode
case 0x5c => noDecoder(AIDamage) case 0x5c => noDecoder(AIDamage)
case 0x5d => game.DeployObjectMessage.decode case 0x5d => game.DeployObjectMessage.decode
case 0x5e => game.FavoritesRequest.decode case 0x5e => game.FavoritesRequest.decode

View file

@ -3,10 +3,32 @@ package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.{Angular, PlanetSideGUID, Vector3} import net.psforever.types.{Angular, PlanetSideGUID, Vector3}
import scodec.Attempt.Successful
import scodec.Codec import scodec.Codec
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} 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.<br>
* <br>
* 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( final case class DroppodFreefallingMessage(
guid: PlanetSideGUID, guid: PlanetSideGUID,
pos: Vector3, pos: Vector3,
@ -21,25 +43,23 @@ final case class DroppodFreefallingMessage(
} }
object DroppodFreefallingMessage extends Marshallable[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] = ( implicit val codec: Codec[DroppodFreefallingMessage] = (
("guid" | PlanetSideGUID.codec) :: ("guid" | PlanetSideGUID.codec) ::
("pos" | Vector3.codec_float) :: ("pos" | Vector3.codec_float) ::
("vel" | Vector3.codec_float) :: ("vel" | Vector3.codec_float) ::
("pos2" | Vector3.codec_float) :: ("pos2" | Vector3.codec_float) ::
("unkA" | Angular.codec_roll) :: ("orientation1" | rotation) ::
("unkB" | Angular.codec_pitch) :: ("orientation2" | rotation)
("unkC" | Angular.codec_yaw()) :: ).as[DroppodFreefallingMessage]
("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
}
)
} }

View file

@ -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]
}

View file

@ -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.<br>
* <br>
* 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]
}

View file

@ -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.<br>
* <br>
* 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]
}

View file

@ -39,6 +39,7 @@ import scodec.codecs._
* 16 - Max unanchor * 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) * 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) * 21 - Disable MAX special effect (NC shield)
* 28 - Cancel warp queue (see: `DroppodLaunchResponseMessage`)<br>
* 29 - AFK<br> * 29 - AFK<br>
* 30 - back in game<br> * 30 - back in game<br>
* 36 - turn on "Looking for Squad"<br> * 36 - turn on "Looking for Squad"<br>

View file

@ -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. * 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. * It makes its own check whether or not to display that "enter vehicle here" icon on the ground.
* This is called an "entry point." * This is called an "entry point."
* Entry points and seat numbers are not required as one-to-one; * Entry points and mount 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.<br> * multiple entry points can lead to the same mount, such as the driver mount of an ANT.<br>
* <br> * <br>
* The player is not allowed to board anything until the server responds in affirmation. * The player is not allowed to board anything until the server responds in affirmation.
* @param player_guid the player * @param player_guid the player
* @param vehicle_guid the vehicle * @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) final case class MountVehicleMsg(player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID, entry_point: Int)
extends PlanetSideGamePacket { extends PlanetSideGamePacket {

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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.<br>
* <br>
* 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
}
)
}

View file

@ -179,14 +179,14 @@ import scodec.codecs._
* `228 - Player/vehicle leaves black ops`<br> * `228 - Player/vehicle leaves black ops`<br>
* <br> * <br>
* `Vehicles:`<br> * `Vehicles:`<br>
* `10 - Driver seat permissions` * `10 - Driver mount permissions`
* <ul> * <ul>
* <li>0 - Locked</li> * <li>0 - Locked</li>
* <li>1 - Group</li> * <li>1 - Group</li>
* <li>3 - Empire</li> * <li>3 - Empire</li>
* </ul> * </ul>
* `11 - Gunner seat(s) permissions (same)`<br> * `11 - Gunner mount(s) permissions (same)`<br>
* `12 - Passenger seat(s) permissions (same)`<br> * `12 - Passenger mount(s) permissions (same)`<br>
* `13 - Trunk permissions (same)`<br> * `13 - Trunk permissions (same)`<br>
* `21 - Declare a player the vehicle's owner, by globally unique identifier`<br> * `21 - Declare a player the vehicle's owner, by globally unique identifier`<br>
* `22 - Toggles gunner and passenger mount points (1 = hides, 0 = reveals; this also locks their permissions)`<br> * `22 - Toggles gunner and passenger mount points (1 = hides, 0 = reveals; this also locks their permissions)`<br>

View file

@ -15,7 +15,9 @@ import scodec.codecs._
* @param ang the orientation of the vehicle * @param ang the orientation of the vehicle
* @param vel optional movement data * @param vel optional movement data
* @param flying flight information, valid only for a vehicle that can fly when in flight; * @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 * `None`, when landed and for all vehicles that do not fly
* @param unk3 na * @param unk3 na
* @param unk4 na * @param unk4 na

View file

@ -15,7 +15,7 @@ import shapeless.{::, HNil}
* Upon hitting the ground, it opens up, releasing the player, and despawns.<br> * Upon hitting the ground, it opens up, releasing the player, and despawns.<br>
* <br> * <br>
* Although the droppod is not technically a vehicle, it is treated as such by the game. * 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. * There is no entry animation.
* The exit animation is the droppod flowering open as usual. * 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. * Even in its spread open state, the droppod can be re-entered, though it will remain spread open.

View file

@ -55,7 +55,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl
* health should be less than 3/255, or 0%<br> * health should be less than 3/255, or 0%<br>
* -jammered - vehicles will not be jammered by setting this field<br> * -jammered - vehicles will not be jammered by setting this field<br>
* -player_guid the vehicle's (official) owner; * -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 unk3 na
* @param health the amount of health the vehicle has, as a percentage of a filled bar (255) * @param health the amount of health the vehicle has, as a percentage of a filled bar (255)
* @param unk4 na * @param unk4 na
@ -69,7 +69,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl
* see `vehicle_type` * see `vehicle_type`
* @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included; * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included;
* will also include trunk contents; * 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; * @param vehicle_type a modifier for parsing the vehicle data format differently;
* see `vehicle_format_data`; * see `vehicle_format_data`;
* defaults to `Normal` * 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`.<br> * the entries are temporarily formatted into a linked list before being put back into a normal `List`.<br>
* <br> * <br>
* 6 June 2018:<br> * 6 June 2018:<br>
* Due to curious behavior in the vehicle seat access controls, * Due to curious behavior in the vehicle mount access controls,
* please only encode and decode the driver seat even though all seats are currently reachable. * 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 * @param length the distance in bits to the first inventory entry
* @return a `Codec` that translates `InventoryData` * @return a `Codec` that translates `InventoryData`
*/ */
@ -404,8 +404,8 @@ object VehicleData extends Marshallable[VehicleData] {
uint8 >>:~ { size => uint8 >>:~ { size =>
uint2 :: uint2 ::
(inventory_seat_codec( (inventory_seat_codec(
length, //length of stream until current seat length, //length of stream until current mount
CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next seat CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next mount
) >>:~ { seats => ) >>:~ { seats =>
PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist
}) })
@ -450,13 +450,13 @@ object VehicleData extends Marshallable[VehicleData] {
conditional( conditional(
objClass == ObjectClass.avatar, objClass == ObjectClass.avatar,
inventory_seat_codec( inventory_seat_codec(
{ //length of stream until next seat { //length of stream until next mount
length + (seat match { length + (seat match {
case Some(o) => o.bitsize case Some(o) => o.bitsize
case None => 0 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 ).hlist
} }
@ -487,7 +487,7 @@ object VehicleData extends Marshallable[VehicleData] {
* The operation performed by this `Codec` is very similar to `InternalSlot.codec`. * The operation performed by this `Codec` is very similar to `InternalSlot.codec`.
* @param pad the padding offset for the player's name; * @param pad the padding offset for the player's name;
* 0-7 bits; * 0-7 bits;
* this padding value must recalculate for each represented seat * this padding value must recalculate for each represented mount
* @see `CharacterAppearanceData`<br> * @see `CharacterAppearanceData`<br>
* `VehicleData.InitialStreamLengthToSeatEntries`<br> * `VehicleData.InitialStreamLengthToSeatEntries`<br>
* `CumulativeSeatedPlayerNamePadding` * `CumulativeSeatedPlayerNamePadding`

View file

@ -8,8 +8,10 @@ import net.psforever.objects.avatar.Avatar
import net.psforever.objects.{Player, SpawnPoint, Vehicle} import net.psforever.objects.{Player, SpawnPoint, Vehicle}
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.DroppodError
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
import net.psforever.zones.Zones
import scala.collection.mutable import scala.collection.mutable
import scala.util.Random import scala.util.Random
@ -71,6 +73,19 @@ object InterstellarClusterService {
final case class GetPlayers(replyTo: ActorRef[PlayersResponse]) extends Command final case class GetPlayers(replyTo: ActorRef[PlayersResponse]) extends Command
final case class PlayersResponse(players: Seq[Avatar]) 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]) class InterstellarClusterService(context: ActorContext[InterstellarClusterService.Command], _zones: Iterable[Zone])
@ -138,7 +153,7 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
Ordering[Int].reverse Ordering[Int].reverse
) // greatest > least ) // greatest > least
.sortWith { .sortWith {
case ((_, spot1, _), (_, spot2, _)) => case ((_, spot1, _), (_, _, _)) =>
spot1.ActivityBy().contains(faction) // prefer own faction activity spot1.ActivityBy().contains(faction) // prefer own faction activity
} }
.headOption .headOption
@ -210,6 +225,22 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
case None => case None =>
replyTo ! SpawnPointResponse(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 this

View file

@ -21,13 +21,13 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
* relogging (short-term client connectivity issue resolution), and * relogging (short-term client connectivity issue resolution), and
* logout (end-of-life conditions involving the separation of a user from the game world).<br> * logout (end-of-life conditions involving the separation of a user from the game world).<br>
* <br> * <br>
* 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. * 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, * (which can be performed by messaging the service indirectly,
* though sending directly to the monitor is recommended) * though sending directly to the monitor is recommended)
* facilitate the management of persistence. * 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, * 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. * 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 * End of life is mainly managed by the monitors internally
@ -144,7 +144,7 @@ class AccountPersistenceService extends Actor {
*/ */
def CreateNewPlayerToken(name: String): ActorRef = { def CreateNewPlayerToken(name: String): ActorRef = {
val ref = 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 accounts += name -> ref
ref ref
} }
@ -312,7 +312,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor {
* <br> * <br>
* The updates have been providing the zone * The updates have been providing the zone
* and the basic information about the user (player name) has been provided since the beginning * 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 four important vectors are:
* the player avatar is in a vehicle, * the player avatar is in a vehicle,
* the player avatar is standing, * the player avatar is standing,
@ -336,7 +336,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor {
case _ => (None, None) //bad data? case _ => (None, None) //bad data?
}) match { }) match {
case (Some(_), Some(seat)) => case (Some(_), Some(seat)) =>
seat.Occupant = None //unseat seat.unmount(player) //unmount
case _ => ; case _ => ;
} }
PlayerAvatarLogout(avatar, player) PlayerAvatarLogout(avatar, player)

View file

@ -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.
* <br>
* 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
)
}
}

View file

@ -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 _ => ;
}
}

View file

@ -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
}

View file

@ -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)
)
}
}

View file

@ -2,15 +2,11 @@
package net.psforever.services.local package net.psforever.services.local
import akka.actor.{Actor, ActorRef, Props} 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.ce.Deployable
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects._ 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.objects.vital.Vitality
import net.psforever.types.{PlanetSideGUID, Vector3} import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.services.local.support._ 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 net.psforever.services.{GenericEventBus, RemoverActor, Service}
import scala.concurrent.duration._ import scala.concurrent.duration._
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.vehicles.{Utility, UtilityType} import net.psforever.objects.vehicles.{Utility, UtilityType}
import net.psforever.services.support.SupportActor import net.psforever.services.support.SupportActor
import java.util.concurrent.TimeUnit
import scala.concurrent.Await
import scala.concurrent.duration.Duration import scala.concurrent.duration.Duration
class LocalService(zone: Zone) extends Actor { class LocalService(zone: Zone) extends Actor {
@ -91,6 +84,12 @@ class LocalService(zone: Zone) extends Actor {
LocalEvents.publish( LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.DoorCloses(door_guid)) 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) => case LocalAction.HackClear(player_guid, target, unk1, unk2) =>
LocalEvents.publish( LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.SendHackMessageHackCleared(target.GUID, unk1, unk2)) 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) 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) => case LocalAction.SetEmpire(object_guid, empire) =>
LocalEvents.publish( LocalEvents.publish(
LocalServiceResponse( LocalServiceResponse(
@ -130,6 +137,34 @@ class LocalService(zone: Zone) extends Actor {
LocalResponse.SetEmpire(object_guid, empire) 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) => case LocalAction.ToggleTeleportSystem(player_guid, router, system_plan) =>
LocalEvents.publish( LocalEvents.publish(
LocalServiceResponse( LocalServiceResponse(
@ -166,7 +201,7 @@ class LocalService(zone: Zone) extends Actor {
LocalResponse.TriggerSound(sound, pos, unk, volume) LocalResponse.TriggerSound(sound, pos, unk, volume)
) )
) )
case LocalAction.UpdateForceDomeStatus(player_guid, building_guid, activated) => { case LocalAction.UpdateForceDomeStatus(player_guid, building_guid, activated) =>
LocalEvents.publish( LocalEvents.publish(
LocalServiceResponse( LocalServiceResponse(
s"/$forChannel/Local", s"/$forChannel/Local",
@ -174,7 +209,6 @@ class LocalService(zone: Zone) extends Actor {
LocalResponse.UpdateForceDomeStatus(building_guid, activated) LocalResponse.UpdateForceDomeStatus(building_guid, activated)
) )
) )
}
case LocalAction.RechargeVehicleWeapon(player_guid, vehicle_guid, weapon_guid) => case LocalAction.RechargeVehicleWeapon(player_guid, vehicle_guid, weapon_guid) =>
LocalEvents.publish( LocalEvents.publish(
LocalServiceResponse( LocalServiceResponse(
@ -231,9 +265,9 @@ class LocalService(zone: Zone) extends Actor {
if (seats.count(_.isOccupied) > 0) { if (seats.count(_.isOccupied) > 0) {
val wasKickedByDriver = false //TODO yeah, I don't know val wasKickedByDriver = false //TODO yeah, I don't know
seats.foreach(seat => { seats.foreach(seat => {
seat.Occupant match { seat.occupant match {
case Some(tplayer) => case Some(tplayer) =>
seat.Occupant = None seat.unmount(tplayer)
tplayer.VehicleSeated = None tplayer.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage( zone.VehicleEvents ! VehicleServiceMessage(
zone.id, zone.id,

View file

@ -10,7 +10,9 @@ import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.vehicles.Utility import net.psforever.objects.vehicles.Utility
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{DeployableInfo, DeploymentAction, TriggeredSound} import net.psforever.packet.game.{DeployableInfo, DeploymentAction, TriggeredSound}
import net.psforever.services.hart.HartTimer.OrbitalShuttleEvent
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
final case class LocalServiceMessage(forChannel: String, actionMessage: LocalAction.Action) 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 Detonate(guid: PlanetSideGUID, obj: PlanetSideGameObject) extends Action
final case class DoorOpens(player_guid: PlanetSideGUID, continent: Zone, door: Door) 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 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) final case class HackClear(player_guid: PlanetSideGUID, target: PlanetSideServerObject, unk1: Long, unk2: Long = 8L)
extends Action extends Action
final case class HackTemporarily( final case class HackTemporarily(
@ -60,7 +63,16 @@ object LocalAction {
src_guid: PlanetSideGUID, src_guid: PlanetSideGUID,
dest_guid: PlanetSideGUID dest_guid: PlanetSideGUID
) extends Action ) 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 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( final case class ToggleTeleportSystem(
player_guid: PlanetSideGUID, player_guid: PlanetSideGUID,
router: Vehicle, router: Vehicle,

View file

@ -6,9 +6,11 @@ import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.objects.vehicles.Utility import net.psforever.objects.vehicles.Utility
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.services.GenericEventBusMsg import net.psforever.services.GenericEventBusMsg
import net.psforever.services.hart.HartTimer.OrbitalShuttleEvent
final case class LocalServiceResponse( final case class LocalServiceResponse(
channel: String, channel: String,
@ -43,7 +45,16 @@ object LocalResponse {
src_guid: PlanetSideGUID, src_guid: PlanetSideGUID,
dest_guid: PlanetSideGUID dest_guid: PlanetSideGUID
) extends Response ) 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 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( final case class ToggleTeleportSystem(
router: Vehicle, router: Vehicle,
systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)] systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)]

View file

@ -105,6 +105,14 @@ class VehicleService(zone: Zone) extends Actor {
VehicleResponse.KickPassenger(seat_num, kickedByDriver, vehicle_guid) 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) => case VehicleAction.LoadVehicle(player_guid, vehicle, vtype, vguid, vdata) =>
VehicleEvents.publish( VehicleEvents.publish(
VehicleServiceResponse( VehicleServiceResponse(

View file

@ -63,7 +63,7 @@ object VehicleAction {
vdata: ConstructorData vdata: ConstructorData
) extends Action ) extends Action
final case class MountVehicle(player_guid: PlanetSideGUID, object_guid: PlanetSideGUID, seat: Int) 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 Ownership(player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID) extends Action
final case class PlanetsideAttribute( final case class PlanetsideAttribute(
player_guid: PlanetSideGUID, player_guid: PlanetSideGUID,

View file

@ -45,11 +45,12 @@ object VehicleResponse {
final case class LoadVehicle(vehicle: Vehicle, vtype: Int, vguid: PlanetSideGUID, vdata: ConstructorData) final case class LoadVehicle(vehicle: Vehicle, vtype: Int, vguid: PlanetSideGUID, vdata: ConstructorData)
extends Response extends Response
final case class MountVehicle(object_guid: PlanetSideGUID, seat: Int) 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 Ownership(vehicle_guid: PlanetSideGUID) extends Response
final case class PlanetsideAttribute(vehicle_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) final case class PlanetsideAttribute(vehicle_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long)
extends Response extends Response
final case class RevealPlayer(player_guid: PlanetSideGUID) 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 SeatPermissions(vehicle_guid: PlanetSideGUID, seat_group: Int, permission: Long) extends Response
final case class StowEquipment( final case class StowEquipment(
vehicle_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID,
slot: Int, slot: Int,

Some files were not shown because too many files have changed in this diff Show more