mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
Hart (#723)
* 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:
parent
e3de497be3
commit
71ab35ecab
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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] {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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)"))
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 =>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 => ;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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]()
|
||||||
|
}
|
||||||
|
|
@ -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]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 =>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
272
src/main/scala/net/psforever/services/hart/HartEvent.scala
Normal file
272
src/main/scala/net/psforever/services/hart/HartEvent.scala
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/scala/net/psforever/services/hart/HartService.scala
Normal file
50
src/main/scala/net/psforever/services/hart/HartService.scala
Normal 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 _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
286
src/main/scala/net/psforever/services/hart/HartTimer.scala
Normal file
286
src/main/scala/net/psforever/services/hart/HartTimer.scala
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue