Deployable Behaviors (#840)

* unifying the split code pathways that separated telepads from other deloyables; in other words, no more SimpleDeployables and ComplexDeployables, just Deployables

* moved some aspects of the build logic into a deployable control mixin; aspects governing the deplpoyable toolbox have been transferred into the player control agency

* moving aspects of teleportation system establishment and decomposition into specialized Telepad control agencies

* retiring deployable disposal code path that required a dedicated remover; each deployable now handles its own removal, and some do special things when being removed; process still has some rough edges and tests are probably thoroughly broken

* additional modifications to support boomers and telepads; consolidation of code for deployable acknowledgement by owner and during failure conditions; tests for behavior

* retooled a significant portion of the build sequence and deconstruct sequence to: eliminate duplicate messages, give the player more input to and control over the process, remove undue responsibility thrust on SessionActor

* messaging issue where player did not re-raise hand after exchanging a used construction tool for a new construction tool

* modification to deconstruct path to make certain deplayble is unregistered last; ridding requirement of AlertDestroyDeployable; fixing test

* create paths for unowned deployable building and (standard) owned deployable building; corrected activation and connection between telepad deployable and internal roouter telepad; wrote tests for connection between telepad deployable and internal telepad

* modifiying the conditions of a deployable construction item being moved into a visible player slot such that the construction item's initial output is valid given the player's current certifications

* by forcing the fire mode to revert briefly before the ammo type updates, the construction item can be made to remain consistent between fire mode shifts

* construction tools now keep track of fire mode ammo types for a period of time, allowing one mode's last setting to be retained

* greatly delayed rebase with master

* minor changes; test correction (?)

* router is go?
This commit is contained in:
Fate-JH 2021-06-02 11:51:38 -04:00 committed by GitHub
parent 7b4f955cbf
commit 2f9c4a7cf2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 2825 additions and 2124 deletions

View file

@ -7,7 +7,7 @@ import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.structures.{Building, StructureType}
import net.psforever.objects.zones.Zone
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.collection.mutable.ListBuffer
@ -44,10 +44,10 @@ object ZoneActor {
final case class PickupItem(guid: PlanetSideGUID) extends Command
final case class BuildDeployable(obj: PlanetSideGameObject with Deployable, withTool: ConstructionItem)
final case class BuildDeployable(obj: Deployable, withTool: ConstructionItem)
extends Command
final case class DismissDeployable(obj: PlanetSideGameObject with Deployable) extends Command
final case class DismissDeployable(obj: Deployable) extends Command
final case class SpawnVehicle(vehicle: Vehicle) extends Command
@ -113,7 +113,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
zone.Ground ! Zone.Ground.PickupItem(guid)
case BuildDeployable(obj, tool) =>
zone.Deployables ! Zone.Deployable.Build(obj, tool)
zone.Deployables ! Zone.Deployable.Build(obj)
case DismissDeployable(obj) =>
zone.Deployables ! Zone.Deployable.Dismiss(obj)

View file

@ -883,4 +883,22 @@ object WorldSession {
false
}
}
def CallBackForTask(task: TaskResolver.GiveTask, sendTo: ActorRef, pass: Any): TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localDesc = task.task.Description
private val destination = sendTo
private val passMsg = pass
override def Description: String = s"callback for tasking $localDesc"
def Execute(resolver: ActorRef): Unit = {
destination ! passMsg
resolver ! Success(this)
}
},
List(task)
)
}
}

View file

@ -1,7 +1,20 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
class BoomerDeployable(cdef: ExplosiveDeployableDefinition) extends ExplosiveDeployable(cdef) {
import akka.actor.{ActorContext, Props}
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.guid.GUIDTask
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.vital.etc.TriggerUsedReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.Zone
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
class BoomerDeployable(cdef: ExplosiveDeployableDefinition)
extends ExplosiveDeployable(cdef) {
private var trigger: Option[BoomerTrigger] = None
def Trigger: Option[BoomerTrigger] = trigger
@ -20,3 +33,75 @@ class BoomerDeployable(cdef: ExplosiveDeployableDefinition) extends ExplosiveDep
Trigger
}
}
class BoomerDeployableDefinition(private val objectId: Int) extends ExplosiveDeployableDefinition(objectId) {
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[BoomerDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
}
object BoomerDeployableDefinition {
def apply(dtype: DeployedItem.Value): BoomerDeployableDefinition = {
new BoomerDeployableDefinition(dtype.id)
}
}
class BoomerDeployableControl(mine: BoomerDeployable)
extends ExplosiveDeployableControl(mine) {
override def receive: Receive =
deployableBehavior
.orElse(takesDamage)
.orElse {
case CommonMessages.Use(player, Some(trigger: BoomerTrigger)) if mine.Trigger.contains(trigger) =>
// the trigger damages the mine, which sets it off, which causes an explosion
// think of this as an initiator to the proper explosion
mine.Destroyed = true
ExplosiveDeployableControl.DamageResolution(
mine,
DamageInteraction(
SourceEntry(mine),
TriggerUsedReason(PlayerSource(player), trigger.GUID),
mine.Position
).calculate()(mine),
damage = 0
)
case _ => ;
}
override def loseOwnership(faction: PlanetSideEmpire.Value): Unit = {
super.loseOwnership(PlanetSideEmpire.NEUTRAL)
mine.OwnerName = None
}
override def gainOwnership(player: Player): Unit = {
mine.Faction = PlanetSideEmpire.NEUTRAL //force map icon redraw
super.gainOwnership(player, player.Faction)
}
override def dismissDeployable() : Unit = {
super.dismissDeployable()
val zone = mine.Zone
mine.Trigger match {
case Some(trigger) =>
mine.Trigger = None
trigger.Companion = None
val guid = trigger.GUID
Zone.EquipmentIs.Where(trigger, guid, zone) match {
case Some(Zone.EquipmentIs.InContainer(container, index)) =>
container.Slot(index).Equipment = None
case Some(Zone.EquipmentIs.OnGround()) =>
zone.Ground ! Zone.Ground.RemoveItem(guid)
case _ => ;
}
zone.AvatarEvents! AvatarServiceMessage(
zone.id,
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, trigger.GUID)
)
zone.tasks ! GUIDTask.UnregisterObjectTask(trigger)(zone.GUID)
case None => ;
}
}
}

View file

@ -24,7 +24,7 @@ class ConstructionItem(private val cItemDef: ConstructionItemDefinition)
extends Equipment
with FireModeSwitch[ConstructionFireMode] {
private var fireModeIndex: Int = 0
private var ammoTypeIndex: Int = 0
private val ammoTypeIndices: Array[Int] = Array.fill[Int](cItemDef.Modes.size)(elem = 0)
def FireModeIndex: Int = fireModeIndex
@ -37,29 +37,33 @@ class ConstructionItem(private val cItemDef: ConstructionItemDefinition)
def NextFireMode: ConstructionFireMode = {
FireModeIndex = FireModeIndex + 1
ammoTypeIndex = 0
FireMode
}
def AmmoTypeIndex: Int = ammoTypeIndex
def AmmoTypeIndex: Int = ammoTypeIndices(fireModeIndex)
def AmmoTypeIndex_=(index: Int): Int = {
ammoTypeIndex = index % FireMode.Deployables.length
ammoTypeIndices(fireModeIndex) = index % FireMode.Deployables.length
AmmoTypeIndex
}
def AmmoType: DeployedItem.Value = FireMode.Deployables(ammoTypeIndex)
def AmmoType: DeployedItem.Value = FireMode.Deployables(AmmoTypeIndex)
def NextAmmoType: DeployedItem.Value = {
AmmoTypeIndex = AmmoTypeIndex + 1
FireMode.Deployables(ammoTypeIndex)
FireMode.Deployables(AmmoTypeIndex)
}
def ModePermissions: Set[Certification] = FireMode.Permissions(ammoTypeIndex)
def ModePermissions: Set[Certification] = FireMode.Permissions(AmmoTypeIndex)
def resetAmmoTypes(): Unit = {
ammoTypeIndices.indices.foreach { index => ammoTypeIndices.update(index, 0) }
}
def Definition: ConstructionItemDefinition = cItemDef
}
object ConstructionItem {
def apply(cItemDef: ConstructionItemDefinition): ConstructionItem = {
new ConstructionItem(cItemDef)

View file

@ -7,18 +7,17 @@ import net.psforever.objects.avatar.{Avatar, Certification}
import scala.concurrent.duration._
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{DeployableInfo, DeploymentAction}
import net.psforever.packet.game._
import net.psforever.types.PlanetSideGUID
import net.psforever.services.RemoverActor
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
object Deployables {
//private val log = org.log4s.getLogger("Deployables")
object Make {
def apply(item: DeployedItem.Value): () => PlanetSideGameObject with Deployable = cemap(item)
def apply(item: DeployedItem.Value): () => Deployable = cemap(item)
private val cemap: Map[DeployedItem.Value, () => PlanetSideGameObject with Deployable] = Map(
private val cemap: Map[DeployedItem.Value, () => Deployable] = Map(
DeployedItem.boomer -> { () => new BoomerDeployable(GlobalDefinitions.boomer) },
DeployedItem.he_mine -> { () => new ExplosiveDeployable(GlobalDefinitions.he_mine) },
DeployedItem.jammer_mine -> { () => new ExplosiveDeployable(GlobalDefinitions.jammer_mine) },
@ -48,6 +47,22 @@ object Deployables {
).withDefaultValue({ () => new ExplosiveDeployable(GlobalDefinitions.boomer) })
}
/**
* Distribute information that a deployable has been destroyed.
* Additionally, since the player who destroyed the deployable isn't necessarily the owner,
* and the real owner will still be aware of the existence of the deployable,
* that player must be informed of the loss of the deployable directly.
* @see `AnnounceDestroyDeployable(Deployable)`
* @see `Deployable.Deconstruct`
* @param target the deployable that is destroyed
* @param time length of time that the deployable is allowed to exist in the game world;
* `None` indicates the normal un-owned existence time (180 seconds)
*/
def AnnounceDestroyDeployable(target: Deployable, time: Option[FiniteDuration]): Unit = {
AnnounceDestroyDeployable(target)
target.Actor ! Deployable.Deconstruct(time)
}
/**
* Distribute information that a deployable has been destroyed.
* The deployable may not have yet been eliminated from the game world (client or server),
@ -58,36 +73,36 @@ object Deployables {
* This function eventually invokes the same routine
* but mainly goes into effect when the deployable has been destroyed
* and may still leave a physical component in the game world to be cleaned up later.
* That is the task `EliminateDeployable` performs.
* Additionally, since the player who destroyed the deployable isn't necessarily the owner,
* and the real owner will still be aware of the existence of the deployable,
* that player must be informed of the loss of the deployable directly.
* @see `DeployableRemover`
* @see `Vitality.DamageResolution`
* @see `LocalResponse.EliminateDeployable`
* @see `DeconstructDeployable`
* @see `DeployableInfo`
* @see `DeploymentAction`
* @see `LocalAction.DeployableMapIcon`
* @param target the deployable that is destroyed
* @param time length of time that the deployable is allowed to exist in the game world;
* `None` indicates the normal un-owned existence time (180 seconds)
*/
def AnnounceDestroyDeployable(target: PlanetSideGameObject with Deployable, time: Option[FiniteDuration]): Unit = {
**/
def AnnounceDestroyDeployable(target: Deployable): Unit = {
val zone = target.Zone
val events = zone.LocalEvents
val item = target.Definition.Item
target.OwnerName match {
case Some(owner) =>
zone.Players.find { p => owner.equals(p.name) } match {
case Some(p) =>
if (p.deployables.Remove(target)) {
events ! LocalServiceMessage(owner, LocalAction.DeployableUIFor(item))
}
case None => ;
}
target.Owner = None
target.OwnerName = None
zone.LocalEvents ! LocalServiceMessage(owner, LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target))
case None => ;
}
zone.LocalEvents ! LocalServiceMessage(
events ! LocalServiceMessage(
s"${target.Faction}",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(target.GUID, Deployable.Icon(target.Definition.Item), target.Position, PlanetSideGUID(0))
DeployableInfo(target.GUID, Deployable.Icon(item), target.Position, PlanetSideGUID(0))
)
)
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), zone))
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, zone, time))
}
/**
@ -99,28 +114,16 @@ object Deployables {
* @return all previously-owned deployables after they have been processed;
* boomers are listed before all other deployable types
*/
def Disown(zone: Zone, avatar: Avatar, replyTo: ActorRef): List[PlanetSideGameObject with Deployable] = {
val (boomers, deployables) =
avatar.deployables
.Clear()
.map(zone.GUID)
.collect { case Some(obj) => obj.asInstanceOf[PlanetSideGameObject with Deployable] }
.partition(_.isInstanceOf[BoomerDeployable])
//do not change the OwnerName field at this time
boomers.collect({
case obj: BoomerDeployable =>
zone.LocalEvents.tell(
LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone, Some(0 seconds))),
replyTo
) //near-instant
obj.Owner = None
obj.Trigger = None
})
deployables.foreach(obj => {
zone.LocalEvents.tell(LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone)), replyTo) //normal decay
obj.Owner = None
})
boomers ++ deployables
def Disown(zone: Zone, avatar: Avatar, replyTo: ActorRef): List[Deployable] = {
avatar.deployables
.Clear()
.map(zone.GUID)
.collect {
case Some(obj: Deployable) =>
obj.Actor ! Deployable.Ownership(None)
obj.Owner = None //fast-forward the effect
obj
}
}
/**
@ -139,6 +142,89 @@ object Deployables {
avatar.deployables.UpdateUI()
}
/**
* If the default ammunition mode for the `ConstructionTool` is not supported by the given certifications,
* find a suitable ammunition mode and switch to it internally.
* No special complaint is raised if the `ConstructionItem` itself is completely unsupported.
* @param certs the certification baseline being compared against
* @param obj the `ConstructionItem` entity
* @return `true`, if the ammunition mode of the item has been changed;
* `false`, otherwise
*/
def initializeConstructionAmmoMode(
certs: Set[Certification],
obj: ConstructionItem
): Boolean = {
if (!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions)) {
Deployables.performConstructionItemAmmoChange(certs, obj, obj.AmmoTypeIndex)
} else {
false
}
}
/**
* The custom behavior responding to the packet `ChangeAmmoMessage` for `ConstructionItem` game objects.
* Iterate through sub-modes corresponding to a type of "deployable" as ammunition for this fire mode
* and check each of these sub-modes for their certification requirements to be met before they can be used.
* Additional effort is exerted to ensure that the requirements for the given ammunition are satisfied.
* If no satisfactory combination is achieved, the original state will be restored.
* @see `Certification`
* @see `ChangeAmmoMessage`
* @see `ConstructionItem.ModePermissions`
* @see `Deployables.constructionItemPermissionComparison`
* @param certs the certification baseline being compared against
* @param obj the `ConstructionItem` entity
* @param originalAmmoIndex the starting point ammunition type mode index
* @return `true`, if the ammunition mode of the item has been changed;
* `false`, otherwise
*/
def performConstructionItemAmmoChange(
certs: Set[Certification],
obj: ConstructionItem,
originalAmmoIndex: Int
): Boolean = {
do {
obj.NextAmmoType
} while (
!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions) &&
originalAmmoIndex != obj.AmmoTypeIndex
)
obj.AmmoTypeIndex != originalAmmoIndex
}
/**
* The custom behavior responding to the message `ChangeFireModeMessage` for `ConstructionItem` game objects.
* Each fire mode has sub-modes corresponding to a type of "deployable" as ammunition
* and each of these sub-modes have certification requirements that must be met before they can be used.
* Additional effort is exerted to ensure that the requirements for the given mode and given sub-mode are satisfied.
* If no satisfactory combination is achieved, the original state will be restored.
* @see `Deployables.constructionItemPermissionComparison`
* @see `Deployables.performConstructionItemAmmoChange`
* @see `FireModeSwitch.NextFireMode`
* @param certs the certification baseline being compared against
* @param obj the `ConstructionItem` entity
* @param originalModeIndex the starting point fire mode index
* @return `true`, if the ammunition mode of the item has been changed;
* `false`, otherwise
*/
def performConstructionItemFireModeChange(
certs: Set[Certification],
obj: ConstructionItem,
originalModeIndex: Int
): Boolean = {
/*
if any of the fire modes possess an initial option that is not valid for a given set of certifications,
but a subsequent option is valid, the do...while loop has to be modified to traverse and compare each option
*/
do {
obj.NextFireMode
} while (
!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions) &&
originalModeIndex != obj.FireModeIndex
)
originalModeIndex != obj.FireModeIndex
}
/**
* Compare sets of certifications to determine if
* the requested `Engineering`-like certification requirements of the one group can be found in a another group.

View file

@ -2,19 +2,17 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.ce._
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.geometry.Geometry3D
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.vital.resolution.ResolutionCalculations.Output
import net.psforever.objects.vital.{SimpleResolutions, Vitality}
import net.psforever.objects.vital.etc.TriggerUsedReason
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.Zone
@ -26,13 +24,13 @@ import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.concurrent.duration._
class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition)
extends ComplexDeployable(cdef)
with JammableUnit {
extends Deployable(cdef)
with JammableUnit {
override def Definition: ExplosiveDeployableDefinition = cdef
}
class ExplosiveDeployableDefinition(private val objectId: Int) extends ComplexDeployableDefinition(objectId) {
class ExplosiveDeployableDefinition(private val objectId: Int) extends DeployableDefinition(objectId) {
Name = "explosive_deployable"
DeployCategory = DeployableCategory.Mines
Model = SimpleResolutions.calculate
@ -47,14 +45,10 @@ class ExplosiveDeployableDefinition(private val objectId: Int) extends ComplexDe
DetonateOnJamming
}
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[ExplosiveDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object ExplosiveDeployableDefinition {
@ -63,30 +57,22 @@ object ExplosiveDeployableDefinition {
}
}
class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with Damageable {
class ExplosiveDeployableControl(mine: ExplosiveDeployable)
extends Actor
with DeployableBehavior
with Damageable {
def DeployableObject = mine
def DamageableObject = mine
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
}
def receive: Receive =
takesDamage
deployableBehavior
.orElse(takesDamage)
.orElse {
case CommonMessages.Use(player, Some(trigger: BoomerTrigger)) if {
mine match {
case boomer: BoomerDeployable => boomer.Trigger.contains(trigger) && mine.Definition.Damageable
case _ => false
}
} =>
// the trigger damages the mine, which sets it off, which causes an explosion
// think of this as an initiator to the proper explosion
mine.Destroyed = true
ExplosiveDeployableControl.DamageResolution(
mine,
DamageInteraction(
SourceEntry(mine),
TriggerUsedReason(PlayerSource(player), trigger.GUID),
mine.Position
).calculate()(mine),
damage = 0
)
case _ => ;
}

View file

@ -947,7 +947,7 @@ object GlobalDefinitions {
/*
combat engineering deployables
*/
val boomer = ExplosiveDeployableDefinition(DeployedItem.boomer)
val boomer = BoomerDeployableDefinition(DeployedItem.boomer)
val he_mine = ExplosiveDeployableDefinition(DeployedItem.he_mine)
@ -975,7 +975,7 @@ object GlobalDefinitions {
val deployable_shield_generator = new ShieldGeneratorDefinition
val router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable)
val router_telepad_deployable = TelepadDeployableDefinition(DeployedItem.router_telepad_deployable)
//this is only treated like a deployable
val internal_router_telepad_deployable = InternalTelepadDefinition() //objectId: 744
@ -4993,28 +4993,35 @@ object GlobalDefinitions {
ace.Name = "ace"
ace.Size = EquipmentSize.Pistol
ace.Modes += new ConstructionFireMode
ace.Modes.head.Item(DeployedItem.boomer, Set(Certification.CombatEngineering))
ace.Modes += new ConstructionFireMode
ace.Modes(1).Item(DeployedItem.he_mine, Set(Certification.CombatEngineering))
ace.Modes(1).Item(DeployedItem.jammer_mine, Set(Certification.AssaultEngineering))
ace.Modes += new ConstructionFireMode
ace.Modes(2).Item(DeployedItem.spitfire_turret, Set(Certification.CombatEngineering))
ace.Modes(2).Item(DeployedItem.spitfire_cloaked, Set(Certification.FortificationEngineering))
ace.Modes(2).Item(DeployedItem.spitfire_aa, Set(Certification.FortificationEngineering))
ace.Modes += new ConstructionFireMode
ace.Modes(3).Item(DeployedItem.motionalarmsensor, Set(Certification.CombatEngineering))
ace.Modes(3).Item(DeployedItem.sensor_shield, Set(Certification.AdvancedHacking, Certification.CombatEngineering))
ace.Modes += new ConstructionFireMode {
Item(DeployedItem.boomer, Set(Certification.CombatEngineering))
}
ace.Modes += new ConstructionFireMode {
Item(DeployedItem.he_mine, Set(Certification.CombatEngineering))
Item(DeployedItem.jammer_mine, Set(Certification.AssaultEngineering))
}
ace.Modes += new ConstructionFireMode {
Item(DeployedItem.spitfire_turret, Set(Certification.CombatEngineering))
Item(DeployedItem.spitfire_cloaked, Set(Certification.FortificationEngineering))
Item(DeployedItem.spitfire_aa, Set(Certification.FortificationEngineering))
}
ace.Modes += new ConstructionFireMode {
Item(DeployedItem.motionalarmsensor, Set(Certification.CombatEngineering))
Item(DeployedItem.sensor_shield, Set(Certification.AdvancedHacking, Certification.CombatEngineering))
}
ace.Tile = InventoryTile.Tile33
advanced_ace.Name = "advanced_ace"
advanced_ace.Size = EquipmentSize.Rifle
advanced_ace.Modes += new ConstructionFireMode
advanced_ace.Modes.head.Item(DeployedItem.tank_traps, Set(Certification.FortificationEngineering))
advanced_ace.Modes += new ConstructionFireMode
advanced_ace.Modes(1).Item(DeployedItem.portable_manned_turret, Set(Certification.AssaultEngineering))
advanced_ace.Modes += new ConstructionFireMode
advanced_ace.Modes(2).Item(DeployedItem.deployable_shield_generator, Set(Certification.AssaultEngineering))
advanced_ace.Modes += new ConstructionFireMode {
Item(DeployedItem.portable_manned_turret, Set(Certification.AssaultEngineering))
}
advanced_ace.Modes += new ConstructionFireMode {
Item(DeployedItem.tank_traps, Set(Certification.FortificationEngineering))
}
advanced_ace.Modes += new ConstructionFireMode {
Item(DeployedItem.deployable_shield_generator, Set(Certification.AssaultEngineering))
}
advanced_ace.Tile = InventoryTile.Tile93
router_telepad.Name = "router_telepad"
@ -7041,6 +7048,7 @@ object GlobalDefinitions {
val smallTurret = GeometryForm.representByCylinder(radius = 0.48435f, height = 1.23438f) _
val sensor = GeometryForm.representByCylinder(radius = 0.1914f, height = 1.21875f) _
val largeTurret = GeometryForm.representByCylinder(radius = 0.8437f, height = 2.29687f) _
boomer.Name = "boomer"
boomer.Descriptor = "Boomers"
boomer.MaxHealth = 100
@ -7049,6 +7057,7 @@ object GlobalDefinitions {
boomer.Repairable = false
boomer.DeployCategory = DeployableCategory.Boomers
boomer.DeployTime = Duration.create(1000, "ms")
boomer.deployAnimation = DeployAnimation.Standard
boomer.explodes = true
boomer.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.Splash
@ -7063,6 +7072,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
boomer.Geometry = mine
he_mine.Name = "he_mine"
he_mine.Descriptor = "Mines"
he_mine.MaxHealth = 100
@ -7070,6 +7080,7 @@ object GlobalDefinitions {
he_mine.DamageableByFriendlyFire = false
he_mine.Repairable = false
he_mine.DeployTime = Duration.create(1000, "ms")
he_mine.deployAnimation = DeployAnimation.Standard
he_mine.explodes = true
he_mine.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.Splash
@ -7084,6 +7095,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
he_mine.Geometry = mine
jammer_mine.Name = "jammer_mine"
jammer_mine.Descriptor = "JammerMines"
jammer_mine.MaxHealth = 100
@ -7091,8 +7103,10 @@ object GlobalDefinitions {
jammer_mine.DamageableByFriendlyFire = false
jammer_mine.Repairable = false
jammer_mine.DeployTime = Duration.create(1000, "ms")
jammer_mine.deployAnimation = DeployAnimation.Standard
jammer_mine.DetonateOnJamming = false
jammer_mine.Geometry = mine
spitfire_turret.Name = "spitfire_turret"
spitfire_turret.Descriptor = "Spitfires"
spitfire_turret.MaxHealth = 100
@ -7105,7 +7119,7 @@ object GlobalDefinitions {
spitfire_turret.DeployCategory = DeployableCategory.SmallTurrets
spitfire_turret.DeployTime = Duration.create(5000, "ms")
spitfire_turret.Model = ComplexDeployableResolutions.calculate
spitfire_turret.explodes = true
spitfire_turret.deployAnimation = DeployAnimation.Standard
spitfire_turret.explodes = true
spitfire_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@ -7116,6 +7130,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
spitfire_turret.Geometry = smallTurret
spitfire_cloaked.Name = "spitfire_cloaked"
spitfire_cloaked.Descriptor = "CloakingSpitfires"
spitfire_cloaked.MaxHealth = 100
@ -7127,6 +7142,7 @@ object GlobalDefinitions {
spitfire_cloaked.ReserveAmmunition = false
spitfire_cloaked.DeployCategory = DeployableCategory.SmallTurrets
spitfire_cloaked.DeployTime = Duration.create(5000, "ms")
spitfire_cloaked.deployAnimation = DeployAnimation.Standard
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate
spitfire_cloaked.explodes = true
spitfire_cloaked.innateDamage = new DamageWithPosition {
@ -7138,6 +7154,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
spitfire_cloaked.Geometry = smallTurret
spitfire_aa.Name = "spitfire_aa"
spitfire_aa.Descriptor = "FlakSpitfires"
spitfire_aa.MaxHealth = 100
@ -7149,6 +7166,7 @@ object GlobalDefinitions {
spitfire_aa.ReserveAmmunition = false
spitfire_aa.DeployCategory = DeployableCategory.SmallTurrets
spitfire_aa.DeployTime = Duration.create(5000, "ms")
spitfire_aa.deployAnimation = DeployAnimation.Standard
spitfire_aa.Model = ComplexDeployableResolutions.calculate
spitfire_aa.explodes = true
spitfire_aa.innateDamage = new DamageWithPosition {
@ -7160,6 +7178,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
spitfire_aa.Geometry = smallTurret
motionalarmsensor.Name = "motionalarmsensor"
motionalarmsensor.Descriptor = "MotionSensors"
motionalarmsensor.MaxHealth = 100
@ -7167,7 +7186,9 @@ object GlobalDefinitions {
motionalarmsensor.Repairable = true
motionalarmsensor.RepairIfDestroyed = false
motionalarmsensor.DeployTime = Duration.create(1000, "ms")
motionalarmsensor.deployAnimation = DeployAnimation.Standard
motionalarmsensor.Geometry = sensor
sensor_shield.Name = "sensor_shield"
sensor_shield.Descriptor = "SensorShields"
sensor_shield.MaxHealth = 100
@ -7175,7 +7196,9 @@ object GlobalDefinitions {
sensor_shield.Repairable = true
sensor_shield.RepairIfDestroyed = false
sensor_shield.DeployTime = Duration.create(5000, "ms")
sensor_shield.deployAnimation = DeployAnimation.Standard
sensor_shield.Geometry = sensor
tank_traps.Name = "tank_traps"
tank_traps.Descriptor = "TankTraps"
tank_traps.MaxHealth = 5000
@ -7184,6 +7207,7 @@ object GlobalDefinitions {
tank_traps.RepairIfDestroyed = false
tank_traps.DeployCategory = DeployableCategory.TankTraps
tank_traps.DeployTime = Duration.create(6000, "ms")
tank_traps.deployAnimation = DeployAnimation.Fdu
//tank_traps do not explode
tank_traps.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@ -7194,6 +7218,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f)
val fieldTurretConverter = new FieldTurretConverter
portable_manned_turret.Name = "portable_manned_turret"
portable_manned_turret.Descriptor = "FieldTurrets"
@ -7211,6 +7236,7 @@ object GlobalDefinitions {
portable_manned_turret.Packet = fieldTurretConverter
portable_manned_turret.DeployCategory = DeployableCategory.FieldTurrets
portable_manned_turret.DeployTime = Duration.create(6000, "ms")
portable_manned_turret.deployAnimation = DeployAnimation.Fdu
portable_manned_turret.Model = ComplexDeployableResolutions.calculate
portable_manned_turret.explodes = true
portable_manned_turret.innateDamage = new DamageWithPosition {
@ -7222,6 +7248,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
portable_manned_turret.Geometry = largeTurret
portable_manned_turret_nc.Name = "portable_manned_turret_nc"
portable_manned_turret_nc.Descriptor = "FieldTurrets"
portable_manned_turret_nc.MaxHealth = 1000
@ -7238,6 +7265,7 @@ object GlobalDefinitions {
portable_manned_turret_nc.Packet = fieldTurretConverter
portable_manned_turret_nc.DeployCategory = DeployableCategory.FieldTurrets
portable_manned_turret_nc.DeployTime = Duration.create(6000, "ms")
portable_manned_turret_nc.deployAnimation = DeployAnimation.Fdu
portable_manned_turret_nc.Model = ComplexDeployableResolutions.calculate
portable_manned_turret_nc.explodes = true
portable_manned_turret_nc.innateDamage = new DamageWithPosition {
@ -7249,6 +7277,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
portable_manned_turret_nc.Geometry = largeTurret
portable_manned_turret_tr.Name = "portable_manned_turret_tr"
portable_manned_turret_tr.Descriptor = "FieldTurrets"
portable_manned_turret_tr.MaxHealth = 1000
@ -7265,6 +7294,7 @@ object GlobalDefinitions {
portable_manned_turret_tr.Packet = fieldTurretConverter
portable_manned_turret_tr.DeployCategory = DeployableCategory.FieldTurrets
portable_manned_turret_tr.DeployTime = Duration.create(6000, "ms")
portable_manned_turret_tr.deployAnimation = DeployAnimation.Fdu
portable_manned_turret_tr.Model = ComplexDeployableResolutions.calculate
portable_manned_turret_tr.explodes = true
portable_manned_turret_tr.innateDamage = new DamageWithPosition {
@ -7276,6 +7306,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
portable_manned_turret_tr.Geometry = largeTurret
portable_manned_turret_vs.Name = "portable_manned_turret_vs"
portable_manned_turret_vs.Descriptor = "FieldTurrets"
portable_manned_turret_vs.MaxHealth = 1000
@ -7292,6 +7323,7 @@ object GlobalDefinitions {
portable_manned_turret_vs.Packet = fieldTurretConverter
portable_manned_turret_vs.DeployCategory = DeployableCategory.FieldTurrets
portable_manned_turret_vs.DeployTime = Duration.create(6000, "ms")
portable_manned_turret_vs.deployAnimation = DeployAnimation.Fdu
portable_manned_turret_vs.Model = ComplexDeployableResolutions.calculate
portable_manned_turret_vs.explodes = true
portable_manned_turret_vs.innateDamage = new DamageWithPosition {
@ -7303,6 +7335,7 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
portable_manned_turret_vs.Geometry = largeTurret
deployable_shield_generator.Name = "deployable_shield_generator"
deployable_shield_generator.Descriptor = "ShieldGenerators"
deployable_shield_generator.MaxHealth = 1700
@ -7310,8 +7343,10 @@ object GlobalDefinitions {
deployable_shield_generator.Repairable = true
deployable_shield_generator.RepairIfDestroyed = false
deployable_shield_generator.DeployTime = Duration.create(6000, "ms")
deployable_shield_generator.deployAnimation = DeployAnimation.Fdu
deployable_shield_generator.Model = ComplexDeployableResolutions.calculate
deployable_shield_generator.Geometry = GeometryForm.representByCylinder(radius = 0.6562f, height = 2.17188f)
router_telepad_deployable.Name = "router_telepad_deployable"
router_telepad_deployable.MaxHealth = 100
router_telepad_deployable.Damageable = true
@ -7321,6 +7356,7 @@ object GlobalDefinitions {
router_telepad_deployable.Packet = new TelepadDeployableConverter
router_telepad_deployable.Model = SimpleResolutions.calculate
router_telepad_deployable.Geometry = GeometryForm.representByRaisedSphere(radius = 1.2344f)
internal_router_telepad_deployable.Name = "router_telepad_deployable"
internal_router_telepad_deployable.MaxHealth = 1
internal_router_telepad_deployable.Damageable = false

View file

@ -2,6 +2,7 @@
package net.psforever.objects
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
@ -202,7 +203,7 @@ class Player(var avatar: Avatar)
def HolsterItems(): List[InventoryItem] = holsters
.zipWithIndex
.collect {
case out @ (slot: EquipmentSlot, index: Int) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index)
case (slot: EquipmentSlot, index: Int) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index)
}.toList
def Inventory: GridInventory = inventory
@ -538,11 +539,11 @@ class Player(var avatar: Avatar)
override def toString: String = {
val guid = if (HasGUID) {
s" ${Continent}-${GUID.guid}"
s" $Continent-${GUID.guid}"
} else {
""
}
s"${avatar.name}$guid ${avatar.faction} H: ${Health}/${MaxHealth} A: ${Armor}/${MaxArmor}"
s"${avatar.name}$guid ${avatar.faction} H: $Health/$MaxHealth A: $Armor/$MaxArmor"
}
}
@ -551,6 +552,10 @@ object Player {
final val FreeHandSlot: Int = 250
final val HandsDownSlot: Int = 255
final case class BuildDeployable(obj: Deployable, withTool: ConstructionItem)
final case class LoseDeployable(obj: Deployable)
final case class Die(reason: Option[DamageInteraction])
object Die {

View file

@ -2,14 +2,20 @@
package net.psforever.objects
import net.psforever.objects.avatar.Certification
import net.psforever.login.WorldSession.FindEquipmentStock
import net.psforever.objects.avatar.PlayerControl
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.ExoSuitDefinition
import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.guid.GUIDTask
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.InfantryLoadout
import net.psforever.packet.game.{InventoryStateMessage, RepairMessage}
import net.psforever.types.{ExoSuitType, Vector3}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.annotation.tailrec
@ -123,6 +129,17 @@ object Players {
}
}
/**
* The player may don this exo-suit if the exo-suit has no requirements
* or if the player has fulfilled the requirements of the exo-suit.
* The "requirements" are certification purchases.
* @param player the player
* @param exosuit the exo-suit the player is trying to wear
* @param subtype the variant of this exo-suit type;
* matters for mechanized assault exo-suits, mainly
* @return `true`, if the player and the exo-suit are compatible;
* `false`, otherwise
*/
def CertificationToUseExoSuit(player: Player, exosuit: ExoSuitType.Value, subtype: Int): Boolean = {
ExoSuitDefinition.Select(exosuit, player.Faction).Permissions match {
case Nil =>
@ -148,16 +165,296 @@ object Players {
*/
def repairModifierLevel(player: Player): Int = {
val certs = player.avatar.certifications
if(certs.contains(Certification.AdvancedEngineering) ||
certs.contains(Certification.AssaultEngineering) ||
certs.contains(Certification.FortificationEngineering)) {
if (certs.contains(Certification.AdvancedEngineering) ||
certs.contains(Certification.AssaultEngineering) ||
certs.contains(Certification.FortificationEngineering)) {
3
} else if (certs.contains(Certification.CombatEngineering)) {
}
else if (certs.contains(Certification.CombatEngineering)) {
2
} else if (certs.contains(Certification.Engineering)) {
}
else if (certs.contains(Certification.Engineering)) {
1
} else {
}
else {
0
}
}
/**
* Test whether this deployable can be constructed by this given player.
* The test actually involves a number of checks against numerical limits for supporting the deployable
* (the first of which is whether there is any limit at all).
* Depending on the result against limits successfully, various status messages can be dispatched to the client
* and the deployable will be considered permitted to be constructed.<br>
* <br>
* The first placement limit is the actual number of a specific type of deployable.
* The second placement limit is the actual number of a specific group (category) of deployables.
* Depending on which limit is encountered, an "oldest entry" is struck from the list to make space.
* This generates the first message - "@*OldestDestroyed."
* Another message is generated if the number of that specific type of deployable
* or the number of deployables available in its category matches against the maximum count allowed.
* This generates the second message - "@*LimitReached."
* These messages are mutually exclusive, with "@*OldestDestroyed" taking priority over "@*LimitReached."<br>
* <br>
* Finally, the player needs to actually manage the deployable.
* Once that responsibility is proven, all tests are considered passed.
* @see `ChatMsg`
* @see `DeployableToolbox`
* @see `DeployableToolbox.Add`
* @see `DeployableToolbox.Available`
* @see `DeployableToolbox.CountDeployable`
* @see `DeployableToolbox.DisplaceFirst`
* @see `Deployable.Deconstruct`
* @see `Deployable.Ownership`
* @see `gainDeployableOwnership`
* @see `ObjectDeployedMessage`
* @see `PlayerControl.sendResponse`
* @param player the player that would manage the deployable
* @param obj the deployable
* @return `true`, if the deployable can be constructed under the control of and be supported by the player;
* `false`, otherwise
*/
def deployableWithinBuildLimits(player: Player, obj: Deployable): Boolean = {
val zone = obj.Zone
val channel = player.Name
val definition = obj.Definition
val item = definition.Item
val deployables = player.avatar.deployables
val (curr, max) = deployables.CountDeployable(item)
val tryAddToOwnedDeployables = if (!deployables.Available(obj)) {
val (removed, msg) = {
if (curr == max) { //too many of a specific type of deployable
(deployables.DisplaceFirst(obj), max > 1)
} else if (curr > max) { //somehow we have too many deployables
(None, true)
} else { //make room by eliminating a different type of deployable
(deployables.DisplaceFirst(obj, { d => d.Definition.Item != item }), true)
}
}
removed match {
case Some(telepad: TelepadDeployable) =>
//telepad is not explicitly deconstructed
telepad.Actor ! Deployable.Ownership(None)
true
case Some(old) =>
old.Actor ! Deployable.Deconstruct()
if (msg) { //max test
PlayerControl.sendResponse(
zone,
channel,
ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None)
)
}
true
case None =>
org.log4s.getLogger(name = "Deployables").warn(
s"${player.Name} has no allowance for ${definition.DeployCategory} deployables; is something wrong?"
)
PlayerControl.sendResponse(zone, channel, ObjectDeployedMessage.Failure(definition.Name))
false
}
} else if (obj.isInstanceOf[TelepadDeployable]) {
//always treat the telepad we are putting down as the first and only one
PlayerControl.sendResponse(zone, channel, ObjectDeployedMessage.Success(definition.Name, count = 1, max = 1))
true
} else {
PlayerControl.sendResponse(zone, channel, ObjectDeployedMessage.Success(definition.Name, curr + 1, max))
val (catCurr, catMax) = deployables.CountCategory(item)
if ((max > 1 && curr + 1 == max) || (catMax > 1 && catCurr + 1 == catMax)) {
PlayerControl.sendResponse(
zone,
channel,
ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}LimitReached", None)
)
}
true
}
tryAddToOwnedDeployables && gainDeployableOwnership(player, obj, player.avatar.deployables.Add)
}
/**
* Grant ownership over a deployable to a player and calls for an update to the UI for that deployable.
* Although the formal the ownership change is delayed slightly by messaging protocol,
* the outcome of this function is reliant more on the function parameter
* used to append the deployable to the management system of the to-be-owning player.
* The difference is between technical ownership and indirect knowledge of ownership
* and how these ownership awareness states operate differently on management of the deployable.
* @see `Deployable.Ownership`
* @see `LocalAction.DeployableUIFor`
* @param player the player who would own the deployable
* @param obj the deployable
* @param addFunc the process for assigning management of the deployable to the player
* @return `true`, if the player was assignment management of the deployable;
* `false`, otherwise
*/
def gainDeployableOwnership(
player: Player,
obj: Deployable,
addFunc: Deployable=>Boolean
): Boolean = {
if (player.Zone == obj.Zone && addFunc(obj)) {
obj.Actor ! Deployable.Ownership(player)
player.Zone.LocalEvents ! LocalServiceMessage(player.Name, LocalAction.DeployableUIFor(obj.Definition.Item))
true
} else {
false
}
}
/**
* Common actions related to constructing a new `Deployable` object in the game environment.
* @param zone in which zone these messages apply
* @param channel to whom to send the messages
* @param obj the `Deployable` object
*/
def successfulBuildActivity(zone: Zone, channel: String, obj: Deployable): Unit = {
//sent to avatar event bus to preempt additional tool management
buildCooldownReset(zone, channel, obj)
//sent to local event bus to cooperate with deployable management
zone.LocalEvents ! LocalServiceMessage(
channel,
LocalAction.DeployableUIFor(obj.Definition.Item)
)
}
/**
* Common actions related to constructing a new `Deployable` object in the game environment.
* @param zone in which zone these messages apply
* @param channel to whom to send the messages
* @param obj the `Deployable` object
*/
def buildCooldownReset(zone: Zone, channel: String, obj: Deployable): Unit = {
//sent to avatar event bus to preempt additional tool management
zone.AvatarEvents ! AvatarServiceMessage(
channel,
AvatarAction.SendResponse(Service.defaultPlayerGUID, GenericObjectActionMessage(obj.GUID, 21))
)
}
/**
* Destroy a `ConstructionItem` object that can be found in the indexed slot.
* @see `Player.Find`
* @param tool the `ConstructionItem` object currently in the slot (checked)
* @param index the slot index
*/
def commonDestroyConstructionItem(player: Player, tool: ConstructionItem, index: Int): Unit = {
val zone = player.Zone
if (safelyRemoveConstructionItemFromSlot(player, tool, index, "CommonDestroyConstructionItem")) {
zone.tasks ! GUIDTask.UnregisterEquipment(tool)(zone.GUID)
}
}
/**
* Find the target `ConstructionTool` object, either at the suggested slot or wherever it is on the `player`,
* and remove it from the game world visually.<br>
* <br>
* Not finding the target object at its intended slot is an entirely recoverable situation
* as long as the target object is discovered to be somewhere else in the player's holsters or inventory space.
* If found after a more thorough search, merely log the discrepancy as a warning.
* If the discrepancy becomes common, the developer messed up the function call
* or he should not be using this function.
* @param tool the `ConstructionItem` object currently in the slot (checked)
* @param index the slot index
* @param logDecorator what kind of designation to give any log entires originating from this function;
* defaults to its own function name
* @return `true`, if the target object was found and removed;
* `false`, otherwise
*/
def safelyRemoveConstructionItemFromSlot(
player: Player,
tool: ConstructionItem,
index: Int,
logDecorator: String = "SafelyRemoveConstructionItemFromSlot"
): Boolean = {
if ({
val holster = player.Slot(index)
if (holster.Equipment.contains(tool)) {
holster.Equipment = None
true
} else {
player.Find(tool) match {
case Some(newIndex) =>
log.warn(s"$logDecorator: ${player.Name} was looking for an item in his hand $index, but item was found at $newIndex instead")
player.Slot(newIndex).Equipment = None
true
case None =>
log.warn(s"$logDecorator: ${player.Name} could not find the target ${tool.Definition.Name}")
false
}
}
}) {
val zone = player.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, tool.GUID, 0)
)
true
} else {
false
}
}
/**
* Find a `ConstructionItem` object in player's inventory
* that is the same type as a target `ConstructionItem` object and
* transfer it into the designated slot index, usually a holster.
* Draw that holster.
* After being transferred, the replacement should be reconfigured to match the fire mode of the original.
* The primary use of this operation is following the successful manifestation of a deployable in the game world.<br>
* <br>
* As this function should be used in response to some other action such as actually placing a deployable,
* do not instigate bundling from within the function's scope.
* @see `WorldSessionActor.FinalizeDeployable`<br>
* `FindEquipmentStock`
* @param tool the `ConstructionItem` object to match
* @param index where to put the discovered replacement
*/
def findReplacementConstructionItem(player: Player, tool: ConstructionItem, index: Int): Unit = {
val definition = tool.Definition
if (player.Slot(index).Equipment.isEmpty) {
FindEquipmentStock(player, { e => e.Definition == definition }, 1) match {
case x :: _ =>
val zone = player.Zone
val events = zone.AvatarEvents
val name = player.Name
val pguid = player.GUID
val obj = x.obj.asInstanceOf[ConstructionItem]
if ((player.Slot(index).Equipment = obj).contains(obj)) {
val fireMode = tool.FireModeIndex
val ammoType = tool.AmmoTypeIndex
player.Inventory -= x.start
obj.FireModeIndex = fireMode
//TODO any penalty for being handed an OCM version of the tool?
events ! AvatarServiceMessage(
zone.id,
AvatarAction.EquipmentInHand(Service.defaultPlayerGUID, pguid, index, obj)
)
if (obj.AmmoTypeIndex != ammoType) {
obj.AmmoTypeIndex = ammoType
events ! AvatarServiceMessage(
name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ChangeAmmoMessage(obj.GUID, ammoType))
)
}
if (player.DrawnSlot == Player.HandsDownSlot) {
player.DrawnSlot = index
events ! AvatarServiceMessage(
name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(pguid, index, true))
)
events ! AvatarServiceMessage(
zone.id,
AvatarAction.ObjectHeld(pguid, index)
)
}
}
case Nil => ; //no replacements found
}
} else {
log.warn(
s"FindReplacementConstructionItem: ${player.Name}, your $index hand needs to be empty before a replacement ${definition.Name} can be installed"
)
}
}
}

View file

@ -1,10 +1,10 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.objects.ce._
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
@ -19,22 +19,18 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.duration._
class SensorDeployable(cdef: SensorDeployableDefinition) extends ComplexDeployable(cdef) with Hackable with JammableUnit
class SensorDeployable(cdef: SensorDeployableDefinition) extends Deployable(cdef) with Hackable with JammableUnit
class SensorDeployableDefinition(private val objectId: Int) extends ComplexDeployableDefinition(objectId) {
class SensorDeployableDefinition(private val objectId: Int) extends DeployableDefinition(objectId) {
Name = "sensor_deployable"
DeployCategory = DeployableCategory.Sensors
Model = SimpleResolutions.calculate
Packet = new SmallDeployableConverter
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[SensorDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object SensorDeployableDefinition {
@ -45,15 +41,23 @@ object SensorDeployableDefinition {
class SensorDeployableControl(sensor: SensorDeployable)
extends Actor
with DeployableBehavior
with JammableBehavior
with DamageableEntity
with RepairableEntity {
def DeployableObject = sensor
def JammableObject = sensor
def DamageableObject = sensor
def RepairableObject = sensor
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
}
def receive: Receive =
jammableBehavior
deployableBehavior
.orElse(jammableBehavior)
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse {
@ -106,14 +110,24 @@ class SensorDeployableControl(sensor: SensorDeployable)
override def CancelJammeredStatus(target: Any): Unit = {
target match {
case obj: PlanetSideServerObject with JammableUnit if obj.Jammed =>
sensor.Zone.LocalEvents ! LocalServiceMessage(
sensor.Zone.id,
val zone = sensor.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, true, 1000)
)
case _ => ;
}
super.CancelJammeredStatus(target)
}
override def finalizeDeployable(callback: ActorRef) : Unit = {
super.finalizeDeployable(callback)
val zone = sensor.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", sensor.GUID, true, 1000)
)
}
}
object SensorDeployableControl {
@ -123,7 +137,7 @@ object SensorDeployableControl {
* @param target na
* @param attribution na
*/
def DestructionAwareness(target: Damageable.Target with Deployable, attribution: PlanetSideGUID): Unit = {
def DestructionAwareness(target: Deployable, attribution: PlanetSideGUID): Unit = {
Deployables.AnnounceDestroyDeployable(target, Some(1 seconds))
val zone = target.Zone
zone.LocalEvents ! LocalServiceMessage(

View file

@ -2,8 +2,8 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployableCategory}
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
import net.psforever.objects.serverobject.damage.Damageable.Target
@ -18,35 +18,40 @@ import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
class ShieldGeneratorDeployable(cdef: ShieldGeneratorDefinition)
extends ComplexDeployable(cdef)
extends Deployable(cdef)
with Hackable
with JammableUnit
class ShieldGeneratorDefinition extends ComplexDeployableDefinition(240) {
class ShieldGeneratorDefinition extends DeployableDefinition(240) {
Packet = new ShieldGeneratorConverter
DeployCategory = DeployableCategory.ShieldGenerators
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[ShieldGeneratorControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
class ShieldGeneratorControl(gen: ShieldGeneratorDeployable)
extends Actor
with DeployableBehavior
with JammableBehavior
with DamageableEntity
with RepairableEntity {
def JammableObject = gen
def DamageableObject = gen
def RepairableObject = gen
def DeployableObject = gen
def JammableObject = gen
def DamageableObject = gen
def RepairableObject = gen
deletionType = 1 //from DeployableBehavior
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
}
def receive: Receive =
jammableBehavior
deployableBehavior
.orElse(jammableBehavior)
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse {
@ -166,7 +171,7 @@ object ShieldGeneratorControl {
* @param target na
* @param attribution na
*/
def DestructionAwareness(target: Damageable.Target with Deployable, attribution: PlanetSideGUID): Unit = {
def DestructionAwareness(target: Deployable, attribution: PlanetSideGUID): Unit = {
Deployables.AnnounceDestroyDeployable(target, None)
}
}

View file

@ -134,11 +134,7 @@ object SpecialEmp {
zone: Zone,
obj: PlanetSideGameObject with FactionAffinity with Vitality,
properties: DamageWithPosition
): (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality]) = {
(
zone.DeployableList
.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) => o },
Nil
)
): List[PlanetSideServerObject with Vitality] = {
zone.DeployableList.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) => o }
}
}
}

View file

@ -4,7 +4,9 @@ package net.psforever.objects
import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.definition.ConstructionItemDefinition
class Telepad(private val cdef: ConstructionItemDefinition) extends ConstructionItem(cdef) with TelepadLike
class Telepad(private val cdef: ConstructionItemDefinition)
extends ConstructionItem(cdef)
with TelepadLike
object Telepad {
def apply(cdef: ConstructionItemDefinition): Telepad = {

View file

@ -1,7 +1,139 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.ce.{SimpleDeployable, TelepadLike}
import net.psforever.objects.definition.SimpleDeployableDefinition
import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem, TelepadLike}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.vehicles.UtilityType
import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.vital.SimpleResolutions
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
class TelepadDeployable(ddef: SimpleDeployableDefinition) extends SimpleDeployable(ddef) with TelepadLike
import scala.concurrent.duration._
class TelepadDeployable(ddef: TelepadDeployableDefinition)
extends Deployable(ddef) with TelepadLike {
override def Definition: TelepadDeployableDefinition = ddef
}
class TelepadDeployableDefinition(objectId: Int) extends DeployableDefinition(objectId) {
Model = SimpleResolutions.calculate
var linkTime: FiniteDuration = 60.seconds
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor = context.actorOf(Props(classOf[TelepadDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
}
object TelepadDeployableDefinition {
def apply(dtype: DeployedItem.Value): TelepadDeployableDefinition = {
new TelepadDeployableDefinition(dtype.id)
}
}
class TelepadDeployableControl(tpad: TelepadDeployable)
extends Actor
with DeployableBehavior
with DamageableEntity {
def DeployableObject = tpad
def DamageableObject = tpad
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
TelepadControl.DestructionAwareness(tpad)
}
def receive: Receive =
deployableBehavior
.orElse(takesDamage)
.orElse {
case TelepadLike.Activate(tpad: TelepadDeployable)
if isConstructed.contains(true) =>
val zone = tpad.Zone
(zone.GUID(tpad.Router) match {
case Some(vehicle : Vehicle) => vehicle.Utility(UtilityType.internal_router_telepad_deployable)
case _ => None
}) match {
case Some(obj: InternalTelepad) =>
import scala.concurrent.ExecutionContext.Implicits.global
setup = context.system.scheduler.scheduleOnce(
tpad.Definition.linkTime,
obj.Actor,
TelepadLike.RequestLink(tpad)
)
case _ =>
deconstructDeployable(None)
tpad.OwnerName match {
case Some(owner) =>
TelepadControl.TelepadError(zone, owner, msg = "@Telepad_NoDeploy_RouterLost")
case None => ;
}
}
case TelepadLike.Activate(obj: InternalTelepad)
if isConstructed.contains(true) =>
if (obj.Telepad.contains(tpad.GUID) && tpad.Router.contains(obj.Owner.GUID)) {
tpad.Active = true
TelepadLike.LinkTelepad(tpad.Zone, tpad.GUID)
}
case TelepadLike.SeverLink(obj: InternalTelepad)
if isConstructed.contains(true) =>
if (tpad.Router.contains(obj.Owner.GUID)) {
tpad.Router = None
tpad.Active = false
tpad.Actor ! Deployable.Deconstruct()
}
case _ =>
}
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
TelepadControl.DestructionAwareness(tpad)
Deployables.AnnounceDestroyDeployable(tpad, None)
}
override def startOwnerlessDecay(): Unit = {
//telepads do not decay when they become ownerless
//telepad decay is tied to their lifecycle with routers
tpad.Owner = None
tpad.OwnerName = None
}
override def finalizeDeployable(callback: ActorRef): Unit = {
super.finalizeDeployable(callback)
decay.cancel() //telepad does not decay if unowned; but, deconstruct if router link fails
self ! TelepadLike.Activate(tpad)
}
override def deconstructDeployable(time : Option[FiniteDuration]) : Unit = {
TelepadControl.DestructionAwareness(tpad)
super.deconstructDeployable(time)
}
}
object TelepadControl {
def DestructionAwareness(tpad: TelepadDeployable): Unit = {
if (tpad.Active) {
tpad.Active = false
(tpad.Zone.GUID(tpad.Router) match {
case Some(vehicle : Vehicle) => vehicle.Utility(UtilityType.internal_router_telepad_deployable)
case _ => None
}) match {
case Some(obj: InternalTelepad) => obj.Actor ! TelepadLike.SeverLink(tpad)
case _ => ;
}
}
}
def TelepadError(zone: Zone, channel: String, msg: String): Unit = {
zone.LocalEvents ! LocalServiceMessage(channel, LocalAction.RouterTelepadMessage(msg))
}
}

View file

@ -2,9 +2,9 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem}
import net.psforever.objects.definition.converter.TRAPConverter
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.repair.RepairableEntity
@ -12,19 +12,16 @@ import net.psforever.objects.vital.SimpleResolutions
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
class TrapDeployable(cdef: TrapDeployableDefinition) extends ComplexDeployable(cdef)
class TrapDeployable(cdef: TrapDeployableDefinition)
extends Deployable(cdef)
class TrapDeployableDefinition(objectId: Int) extends ComplexDeployableDefinition(objectId) {
class TrapDeployableDefinition(objectId: Int) extends DeployableDefinition(objectId) {
Model = SimpleResolutions.calculate
Packet = new TRAPConverter
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor = context.actorOf(Props(classOf[TrapDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object TrapDeployableDefinition {
@ -33,12 +30,23 @@ object TrapDeployableDefinition {
}
}
class TrapDeployableControl(trap: TrapDeployable) extends Actor with DamageableEntity with RepairableEntity {
class TrapDeployableControl(trap: TrapDeployable)
extends Actor
with DeployableBehavior
with DamageableEntity
with RepairableEntity {
def DeployableObject = trap
def DamageableObject = trap
def RepairableObject = trap
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
}
def receive: Receive =
takesDamage
deployableBehavior
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse {
case _ =>

View file

@ -2,10 +2,11 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem}
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.definition.converter.SmallTurretConverter
import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit}
import net.psforever.objects.guid.GUIDTask
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
@ -17,9 +18,12 @@ import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.duration.FiniteDuration
class TurretDeployable(tdef: TurretDeployableDefinition)
extends ComplexDeployable(tdef)
extends Deployable(tdef)
with WeaponTurret
with JammableUnit
with Hackable {
@ -29,7 +33,7 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
}
class TurretDeployableDefinition(private val objectId: Int)
extends ComplexDeployableDefinition(objectId)
extends DeployableDefinition(objectId)
with TurretDefinition {
Name = "turret_deployable"
Packet = new SmallTurretConverter
@ -38,17 +42,13 @@ class TurretDeployableDefinition(private val objectId: Int)
Model = SimpleResolutions.calculate
//override to clarify inheritance conflict
override def MaxHealth: Int = super[ComplexDeployableDefinition].MaxHealth
override def MaxHealth: Int = super[DeployableDefinition].MaxHealth
//override to clarify inheritance conflict
override def MaxHealth_=(max: Int): Int = super[ComplexDeployableDefinition].MaxHealth_=(max)
override def MaxHealth_=(max: Int): Int = super[DeployableDefinition].MaxHealth_=(max)
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor = context.actorOf(Props(classOf[TurretControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object TurretDeployableDefinition {
@ -61,11 +61,13 @@ object TurretDeployableDefinition {
class TurretControl(turret: TurretDeployable)
extends Actor
with DeployableBehavior
with FactionAffinityBehavior.Check
with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events
with MountableBehavior
with DamageableWeaponTurret
with RepairableWeaponTurret {
def DeployableObject = turret
def MountableObject = turret
def JammableObject = turret
def FactionObject = turret
@ -74,11 +76,13 @@ class TurretControl(turret: TurretDeployable)
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
damageableWeaponTurretPostStop()
}
def receive: Receive =
checkBehavior
deployableBehavior
.orElse(checkBehavior)
.orElse(jammableBehavior)
.orElse(mountBehavior)
.orElse(dismountBehavior)
@ -99,4 +103,35 @@ class TurretControl(turret: TurretDeployable)
super.DestructionAwareness(target, cause)
Deployables.AnnounceDestroyDeployable(turret, None)
}
override def deconstructDeployable(time: Option[FiniteDuration]) : Unit = {
val zone = turret.Zone
val seats = turret.Seats.values
//either we have no seats or no one gets to sit
val retime = if (seats.count(_.isOccupied) > 0) {
//unlike with vehicles, it's possible to request deconstruction of one's own field turret while seated in it
val wasKickedByDriver = false
seats.foreach { seat =>
seat.occupant match {
case Some(tplayer) =>
seat.unmount(tplayer)
tplayer.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, turret.GUID)
)
case None => ;
}
}
Some(time.getOrElse(Deployable.cleanup) + Deployable.cleanup)
} else {
time
}
super.deconstructDeployable(retime)
}
override def unregisterDeployable(obj: Deployable): Unit = {
val zone = obj.Zone
zone.tasks ! GUIDTask.UnregisterDeployableTurret(turret)(zone.GUID)
}
}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects
import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.transfer.TransferContainer
@ -9,12 +10,12 @@ import net.psforever.objects.vehicles._
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.TriggeredSound
import net.psforever.types.{DriveState, PlanetSideGUID, Vector3}
import net.psforever.services.{RemoverActor, Service}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.duration._
//import scala.concurrent.duration._
object Vehicles {
private val log = org.log4s.getLogger("Vehicles")
@ -309,7 +310,13 @@ object Vehicles {
// If AMS is deployed, swap it to the new faction
target.Definition match {
case GlobalDefinitions.router =>
Vehicles.RemoveTelepads(target)
target.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) =>
//"power cycle"
util.Actor ! TelepadLike.Deactivate(util)
util.Actor ! TelepadLike.Activate(util)
case _ => ;
}
case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed =>
zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone)
case _ => ;
@ -391,25 +398,6 @@ object Vehicles {
}
}
def RemoveTelepads(vehicle: Vehicle): Unit = {
val zone = vehicle.Zone
(vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) =>
val telepad = util.Telepad
util.Telepad = None
zone.GUID(telepad)
case _ =>
None
}) match {
case Some(telepad: TelepadDeployable) =>
log.debug(s"BeforeUnload: deconstructing telepad $telepad that was linked to router $vehicle ...")
telepad.Active = false
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), zone))
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, zone, Some(0 seconds)))
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.

View file

@ -1,9 +1,9 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.avatar
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ce.{Deployable, DeployableCategory, DeployedItem}
import net.psforever.types.PlanetSideGUID
import scala.collection.mutable
/**
@ -13,13 +13,18 @@ import scala.collection.mutable
* `CombatEngineering` and above certifications include permissions for different types of deployables,
* and one unique type of deployable is available through the `GroundSupport`
* and one that also requires `AdvancedHacking`.
* (They are collectively called "ce" for that reason.)
* (They are collectively called "ce" for that reason.)<br>
* <br>
* Not only does the level of certification change the maximum number of deployables that can be managed by type
* but it also influences the maximum number of deployables that can be managed by category.
* Individual deployables are counted by type and category individually in special data structures
* to avoid having to probe the primary list of deployable references whenever a question of quantity is asked.
* As deployables are added and removed, and tracked certifications are added and removed,
* these structures are updated to reflect proper count.
* For example, the greatest number of spitfire turrets that can be placed is 15 (individual count)
* and the greatest number of shadow turrets and cerebus turrets that can be placed is 5 each (individual counts)
* but the maximum number of small turrets that can be placed overall is only 15 (categorical count).
* Spitfire turrets, shadow turrets, and cerebus turrets are all included in the category of small turrets.
*/
class DeployableToolbox {
@ -148,6 +153,28 @@ class DeployableToolbox {
}
}
/**
* Manage the provided deployable, the maximum number of governable units be damned.
* It still needs to be a unique unit of a governable deployable type, however.
* @param obj the deployable
* @return `true`, if the deployable is added;
* `false`, otherwise
*/
def AddOverLimit(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
val category = obj.Definition.DeployCategory
val dCategory = categoryCounts(category)
val dType = deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item))
val dList = deployableLists(category)
if (!dList.contains(obj)) {
dCategory.Current += 1
dType.Current += 1
dList += obj
true
} else {
false
}
}
/**
* Stop managing the provided deployable.<br>
* <br>
@ -156,7 +183,7 @@ class DeployableToolbox {
* If the deployable is found to currently being managed by this toolbox, then it is properly removed.
* No changes should occur if the deployable is not properly removed.
* @param obj the deployable
* @return `true`, if the deployable is added;
* @return `true`, if the deployable is removed;
* `false`, otherwise
*/
def Remove(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
@ -194,7 +221,7 @@ class DeployableToolbox {
*/
def DisplaceFirst(
obj: DeployableToolbox.AcceptableDeployable,
rule: (Deployable) => Boolean
rule: Deployable => Boolean
): Option[DeployableToolbox.AcceptableDeployable] = {
val definition = obj.Definition
val category = definition.DeployCategory
@ -298,7 +325,9 @@ class DeployableToolbox {
List((curr, dType.Current, max, dType.Max))
}
def UpdateUI(): List[(Int, Int, Int, Int)] = DeployedItem.values flatMap UpdateUIElement toList
def UpdateUI(): List[(Int, Int, Int, Int)] = DeployedItem.values.flatMap { value: DeployedItem.Value =>
UpdateUIElement(value)
}.toList
def UpdateUI(entry: Certification): List[(Int, Int, Int, Int)] = {
import Certification._
@ -395,7 +424,7 @@ object DeployableToolbox {
/**
* A `type` intended to properly define the minimum acceptable conditions for a `Deployable` object.
*/
type AcceptableDeployable = PlanetSideGameObject with Deployable
type AcceptableDeployable = Deployable
/**
* An internal class to keep track of the quantity of deployables managed for a certain set of criteria.

View file

@ -3,8 +3,11 @@ package net.psforever.objects.avatar
import akka.actor.{Actor, ActorRef, Props, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
import net.psforever.objects.{Player, _}
import net.psforever.objects.ballistics.PlayerSource
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.DeployAnimation
import net.psforever.objects.equipment._
import net.psforever.objects.guid.GUIDTask
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
@ -22,7 +25,7 @@ import net.psforever.objects.zones._
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.types._
import net.psforever.services.{RemoverActor, Service}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.objects.locker.LockerContainerControl
@ -33,6 +36,7 @@ import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.services.hart.ShuttleState
import net.psforever.packet.PlanetSideGamePacket
import scala.concurrent.duration._
@ -44,8 +48,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
with AggravatedBehavior
with AuraEffectBehavior
with RespondsToZoneEnvironment {
def JammableObject = player
def JammableObject = player
def DamageableObject = player
@ -65,12 +68,12 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath)
SetInteraction(EnvironmentAttribute.GantryDenialField, doInteractingWithGantryField)
SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater)
private[this] val log = org.log4s.getLogger(player.Name)
private[this] val log = org.log4s.getLogger(player.Name)
private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
/** suffocating, or regaining breath? */
var submergedCondition: Option[OxygenState] = None
/** assistance for deployable construction, retention of the construction item */
var deployablePair: Option[(Deployable, ConstructionItem)] = None
/** control agency for the player's locker container (dedicated inventory slot #5) */
val lockerControlAgent: ActorRef = {
val locker = player.avatar.locker
@ -110,20 +113,20 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if item.Definition == GlobalDefinitions.medicalapplicator && player.isAlive =>
//heal
val originalHealth = player.Health
val definition = player.Definition
val definition = player.Definition
if (
player.MaxHealth > 0 && originalHealth < player.MaxHealth &&
user.Faction == player.Faction &&
item.Magazine > 0 &&
Vector3.Distance(user.Position, player.Position) < definition.RepairDistance
) {
val zone = player.Zone
val zone = player.Zone
val events = zone.AvatarEvents
val uname = user.Name
val guid = player.GUID
val uname = user.Name
val guid = player.GUID
if (!(player.isMoving || user.isMoving)) { //only allow stationary heals
val newHealth = player.Health = originalHealth + 10
val magazine = item.Discharge()
val magazine = item.Discharge()
events ! AvatarServiceMessage(
uname,
AvatarAction.SendResponse(
@ -173,17 +176,17 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case CommonMessages.Use(user, Some(item: Tool)) if item.Definition == GlobalDefinitions.bank =>
val originalArmor = player.Armor
val definition = player.Definition
val definition = player.Definition
if (
player.MaxArmor > 0 && originalArmor < player.MaxArmor &&
user.Faction == player.Faction &&
item.AmmoType == Ammo.armor_canister && item.Magazine > 0 &&
Vector3.Distance(user.Position, player.Position) < definition.RepairDistance
) {
val zone = player.Zone
val zone = player.Zone
val events = zone.AvatarEvents
val uname = user.Name
val guid = player.GUID
val uname = user.Name
val guid = player.GUID
if (!(player.isMoving || user.isMoving)) { //only allow stationary repairs
val newArmor = player.Armor =
originalArmor + Repairable.applyLevelModifier(user, item, RepairToolValue(item)).toInt + definition.RepairMod
@ -313,16 +316,16 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}")
val fallbackSubtype = 0
val fallbackSuit = ExoSuitType.Standard
val originalSuit = player.ExoSuit
val fallbackSuit = ExoSuitType.Standard
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
//sanitize exo-suit for change
val dropPred = ContainableBehavior.DropPredicate(player)
val oldHolsters = Players.clearHolsters(player.Holsters().iterator)
val dropHolsters = oldHolsters.filter(dropPred)
val oldInventory = player.Inventory.Clear()
val dropPred = ContainableBehavior.DropPredicate(player)
val oldHolsters = Players.clearHolsters(player.Holsters().iterator)
val dropHolsters = oldHolsters.filter(dropPred)
val oldInventory = player.Inventory.Clear()
val dropInventory = oldInventory.filter(dropPred)
val toDeleteOrDrop: List[InventoryItem] = (player.FreeHand.Equipment match {
val toDeleteOrDrop : List[InventoryItem] = (player.FreeHand.Equipment match {
case Some(obj) =>
val out = InventoryItem(obj, -1)
player.FreeHand.Equipment = None
@ -337,27 +340,27 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//a loadout with a prohibited exo-suit type will result in the fallback exo-suit type
//imposed 5min delay on mechanized exo-suit switches
val (nextSuit, nextSubtype) =
if (
Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) => false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
} else {
true
})
) {
(exosuit, subtype)
if (
Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) => false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
} else {
log.warn(
s"${player.Name} no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead"
)
(fallbackSuit, fallbackSubtype)
}
true
})
) {
(exosuit, subtype)
} else {
log.warn(
s"${player.Name} no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead"
)
(fallbackSuit, fallbackSubtype)
}
//sanitize (incoming) inventory
//TODO equipment permissions; these loops may be expanded upon in future
val curatedHolsters = for {
@ -412,6 +415,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
(finalHolsters, finalInventory)
}
(afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction }
afterHolsters.foreach {
case InventoryItem(citem: ConstructionItem, _) =>
Deployables.initializeConstructionAmmoMode(player.avatar.certifications, citem)
case _ => ;
}
toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL }
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
@ -439,46 +447,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}
case Zone.Ground.ItemOnGround(item, _, _) =>
val name = player.Name
val zone = player.Zone
val avatarEvents = zone.AvatarEvents
val localEvents = zone.LocalEvents
item match {
case trigger: BoomerTrigger =>
//dropped the trigger, no longer own the boomer; make certain whole faction is aware of that
(zone.GUID(trigger.Companion), zone.Players.find { _.name == name }) match {
case (Some(boomer: BoomerDeployable), Some(avatar)) =>
val guid = boomer.GUID
val factionChannel = boomer.Faction.toString
if (avatar.deployables.Remove(boomer)) {
boomer.Faction = PlanetSideEmpire.NEUTRAL
boomer.AssignOwnership(None)
avatar.deployables.UpdateUIElement(boomer.Definition.Item).foreach {
case (currElem, curr, maxElem, max) =>
avatarEvents ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max)
)
avatarEvents ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr)
)
}
localEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, zone))
localEvents ! LocalServiceMessage(
factionChannel,
LocalAction.DeployableMapIcon(
Service.defaultPlayerGUID,
DeploymentAction.Dismiss,
DeployableInfo(guid, DeployableIcon.Boomer, boomer.Position, PlanetSideGUID(0))
)
)
avatarEvents ! AvatarServiceMessage(
factionChannel,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, PlanetSideEmpire.NEUTRAL)
)
}
case _ => ; //pointless trigger? or a trigger being deleted?
//drop the trigger, lose the boomer; make certain whole faction is aware of that
player.Zone.GUID(trigger.Companion) match {
case Some(obj: BoomerDeployable) =>
loseDeployableOwnership(obj)
case _ => ;
}
case _ => ;
}
@ -491,14 +466,85 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case Zone.Ground.CanNotPickupItem(_, item_guid, reason) =>
log.warn(s"${player.Name} failed to pick up an item ($item_guid) from the ground because $reason")
case Player.BuildDeployable(obj: TelepadDeployable, tool: Telepad) =>
obj.Router = tool.Router //necessary; forwards link to the router that prodcued the telepad
setupDeployable(obj, tool)
case Player.BuildDeployable(obj, tool) =>
setupDeployable(obj, tool)
case Zone.Deployable.IsBuilt(obj: BoomerDeployable) =>
deployablePair match {
case Some((deployable, tool)) if deployable eq obj =>
val zone = player.Zone
//boomers
val trigger = new BoomerTrigger
trigger.Companion = obj.GUID
obj.Trigger = trigger
//TODO sufficiently delete the tool
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.ObjectDelete(player.GUID, tool.GUID))
zone.tasks ! GUIDTask.UnregisterEquipment(tool)(zone.GUID)
player.Find(tool) match {
case Some(index) if player.VisibleSlots.contains(index) =>
player.Slot(index).Equipment = None
zone.tasks ! HoldNewEquipmentUp(player)(trigger, index)
case Some(index) =>
player.Slot(index).Equipment = None
zone.tasks ! PutNewEquipmentInInventoryOrDrop(player)(trigger)
case None =>
//don't know where boomer trigger "should" go
zone.tasks ! PutNewEquipmentInInventoryOrDrop(player)(trigger)
}
Players.buildCooldownReset(zone, player.Name, obj)
case _ => ;
}
deployablePair = None
case Zone.Deployable.IsBuilt(obj: TelepadDeployable) =>
deployablePair match {
case Some((deployable, tool: Telepad)) if deployable eq obj =>
RemoveOldEquipmentFromInventory(player)(tool)
val zone = player.Zone
zone.GUID(obj.Router) match {
case Some(v: Vehicle)
if v.Definition == GlobalDefinitions.router => ;
case _ =>
player.Actor ! Player.LoseDeployable(obj)
TelepadControl.TelepadError(zone, player.Name, msg = "@Telepad_NoDeploy_RouterLost")
}
Players.buildCooldownReset(zone, player.Name, obj)
case _ => ;
}
deployablePair = None
case Zone.Deployable.IsBuilt(obj) =>
deployablePair match {
case Some((deployable, tool)) if deployable eq obj =>
Players.buildCooldownReset(player.Zone, player.Name, obj)
player.Find(tool) match {
case Some(index) =>
Players.commonDestroyConstructionItem(player, tool, index)
Players.findReplacementConstructionItem(player, tool, index)
case None =>
log.warn(s"${player.Name} should have destroyed a ${tool.Definition.Name} here, but could not find it")
}
case _ => ;
}
deployablePair = None
case Player.LoseDeployable(obj) =>
if (player.avatar.deployables.Remove(obj)) {
player.Zone.LocalEvents ! LocalServiceMessage(player.Name, LocalAction.DeployableUIFor(obj.Definition.Item))
}
case _ => ;
}
def setExoSuit(exosuit: ExoSuitType.Value, subtype: Int): Boolean = {
var toDelete: List[InventoryItem] = Nil
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
var toDelete : List[InventoryItem] = Nil
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
@ -509,12 +555,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
} else {
}
else {
true
})
if (requestToChangeArmor && allowedToChangeArmor) {
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
val beforeInventory = player.Inventory.Clear()
//change suit
val originalArmor = player.Armor
@ -523,7 +570,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val toArmor = if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), exosuit))
player.Armor = toMaxArmor
} else {
}
else {
player.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
@ -534,7 +582,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
toDelete ++= maxWeapons
normalWeapons
} else {
}
else {
beforeHolsters
}
//populate holsters
@ -543,9 +592,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
normalHolsters,
Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory
)
} else if (originalSuit == exosuit) { //note - this will rarely be the situation
}
else if (originalSuit == exosuit) { //note - this will rarely be the situation
(normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters))
} else {
}
else {
val (afterHolsters, toInventory) =
normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size)
afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj })
@ -558,7 +609,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//put items back into inventory
val (stow, drop) = if (originalSuit == exosuit) {
(finalInventory, Nil)
} else {
}
else {
val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory)
(
a,
@ -590,11 +642,64 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
)
)
true
} else {
}
else {
false
}
}
def loseDeployableOwnership(obj: Deployable): Boolean = {
if (player.avatar.deployables.Remove(obj)) {
obj.Actor ! Deployable.Ownership(None)
player.Zone.LocalEvents ! LocalServiceMessage(player.Name, LocalAction.DeployableUIFor(obj.Definition.Item))
true
}
else {
false
}
}
def setupDeployable(obj: Deployable, tool: ConstructionItem): Unit = {
if (deployablePair.isEmpty) {
val zone = player.Zone
val deployables = player.avatar.deployables
if (deployables.Valid(obj) &&
!deployables.Contains(obj) &&
Players.deployableWithinBuildLimits(player, obj)) {
tool.Definition match {
case GlobalDefinitions.ace | /* animation handled in deployable lifecycle */
GlobalDefinitions.router_telepad => ; /* no special animation */
case GlobalDefinitions.advanced_ace
if obj.Definition.deployAnimation == DeployAnimation.Fdu =>
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PutDownFDU(player.GUID))
case _ =>
org.log4s.getLogger(name = "Deployables").warn(
s"not sure what kind of construction item to animate - ${tool.Definition.Name}"
)
}
deployablePair = Some((obj, tool))
obj.Faction = player.Faction
obj.AssignOwnership(player)
obj.Actor ! Zone.Deployable.Setup()
}
else {
log.warn(s"cannot build a ${obj.Definition.Name}")
DropEquipmentFromInventory(player)(tool, Some(obj.Position))
Players.buildCooldownReset(zone, player.Name, obj)
obj.Position = Vector3.Zero
obj.AssignOwnership(None)
zone.Deployables ! Zone.Deployable.Dismiss(obj)
}
} else {
log.warn(s"already building one deployable, so cannot build a ${obj.Definition.Name}")
obj.Position = Vector3.Zero
obj.AssignOwnership(None)
val zone = player.Zone
zone.Deployables ! Zone.Deployable.Dismiss(obj)
Players.buildCooldownReset(zone, player.Name, obj)
}
}
override protected def PerformDamage(
target: Target,
applyDamageTo: Output
@ -998,7 +1103,31 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val definition = item.Definition
val faction = obj.Faction
val toChannel = if (player.isBackpack) { self.toString } else { name }
val willBeVisible = obj.VisibleSlots.contains(slot)
item.Faction = faction
//handle specific types of items
item match {
case trigger: BoomerTrigger =>
//pick up the trigger, own the boomer; make certain whole faction is aware of that
zone.GUID(trigger.Companion) match {
case Some(obj: BoomerDeployable) =>
val deployables = player.avatar.deployables
if (deployables.Valid(obj)) {
Players.gainDeployableOwnership(player, obj, deployables.AddOverLimit)
}
case _ => ;
}
case citem: ConstructionItem
if willBeVisible =>
if (citem.AmmoTypeIndex > 0) {
//can not preserve ammo type in construction tool packets
citem.resetAmmoTypes()
}
Deployables.initializeConstructionAmmoMode(player.avatar.certifications, citem)
case _ => ;
}
events ! AvatarServiceMessage(
toChannel,
AvatarAction.SendResponse(
@ -1011,56 +1140,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
)
)
)
if (!player.isBackpack && obj.VisibleSlots.contains(slot)) {
if (!player.isBackpack && willBeVisible) {
events ! AvatarServiceMessage(zone.id, AvatarAction.EquipmentInHand(guid, guid, slot, item))
}
//handle specific types of items
item match {
case trigger: BoomerTrigger =>
//pick up the trigger, own the boomer; make certain whole faction is aware of that
(zone.GUID(trigger.Companion), zone.Players.find { _.name == name }) match {
case (Some(boomer: BoomerDeployable), Some(avatar))
if !boomer.OwnerName.contains(name) || boomer.Faction != faction =>
val bguid = boomer.GUID
val faction = player.Faction
val factionChannel = faction.toString
if (avatar.deployables.Add(boomer)) {
boomer.Faction = faction
boomer.AssignOwnership(player)
avatar.deployables.UpdateUIElement(boomer.Definition.Item).foreach {
case (currElem, curr, maxElem, max) =>
events ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max)
)
events ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr)
)
}
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(boomer), zone))
events ! AvatarServiceMessage(
factionChannel,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, bguid, faction)
)
zone.LocalEvents ! LocalServiceMessage(
factionChannel,
LocalAction.DeployableMapIcon(
Service.defaultPlayerGUID,
DeploymentAction.Build,
DeployableInfo(
bguid,
DeployableIcon.Boomer,
boomer.Position,
boomer.Owner.getOrElse(PlanetSideGUID(0))
)
)
)
}
case _ => ; //pointless trigger?
}
case _ => ;
}
}
def SwapItemCallback(item: Equipment, fromSlot: Int): Unit = {
@ -1240,4 +1322,8 @@ object PlayerControl {
case Aura.Fire => 8
case _ => 0
}
def sendResponse(zone: Zone, channel: String, msg: PlanetSideGamePacket): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.SendResponse(Service.defaultPlayerGUID, msg))
}
}

View file

@ -1,55 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.TurretDeployable
import net.psforever.objects.ce.ComplexDeployable
import net.psforever.objects.definition.{DeployableDefinition, ObjectDefinition}
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class ComplexDeployableSource(
obj_def: ObjectDefinition with DeployableDefinition,
faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
ownerName: String,
position: Vector3,
orientation: Vector3
) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj_def.Name)
override def Faction = faction
def Definition: ObjectDefinition with DeployableDefinition = obj_def
def Health = health
def Shields = shields
def OwnerName = ownerName
def Position = position
def Orientation = orientation
def Velocity = None
def Modifiers = obj_def.asInstanceOf[ResistanceProfile]
}
object ComplexDeployableSource {
def apply(obj: ComplexDeployable): ComplexDeployableSource = {
ComplexDeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.OwnerName.getOrElse(""),
obj.Position,
obj.Orientation
)
}
def apply(obj: TurretDeployable): ComplexDeployableSource = {
ComplexDeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.OwnerName.getOrElse(""),
obj.Position,
obj.Orientation
)
}
}

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.{DeployableDefinition, ObjectDefinition}
import net.psforever.objects.vital.resistance.ResistanceProfile
@ -11,6 +10,7 @@ final case class DeployableSource(
obj_def: ObjectDefinition with DeployableDefinition,
faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
ownerName: String,
position: Vector3,
orientation: Vector3
@ -19,6 +19,7 @@ final case class DeployableSource(
override def Faction = faction
def Definition: ObjectDefinition with DeployableDefinition = obj_def
def Health = health
def Shields = shields
def OwnerName = ownerName
def Position = position
def Orientation = orientation
@ -27,11 +28,12 @@ final case class DeployableSource(
}
object DeployableSource {
def apply(obj: PlanetSideGameObject with Deployable): DeployableSource = {
def apply(obj: Deployable): DeployableSource = {
DeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.OwnerName.getOrElse(""),
obj.Position,
obj.Orientation

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.ce.{ComplexDeployable, SimpleDeployable}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle}
import net.psforever.objects.entity.WorldEntity
@ -32,11 +32,10 @@ object SourceEntry {
def apply(target: PlanetSideGameObject with FactionAffinity): SourceEntry = {
target match {
case obj: Player => PlayerSource(obj)
case obj: Vehicle => VehicleSource(obj)
case obj: ComplexDeployable => ComplexDeployableSource(obj)
case obj: SimpleDeployable => DeployableSource(obj)
case _ => ObjectSource(target)
case obj: Player => PlayerSource(obj)
case obj: Vehicle => VehicleSource(obj)
case obj: Deployable => DeployableSource(obj)
case _ => ObjectSource(target)
}
}

View file

@ -1,22 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
import net.psforever.objects.definition.ComplexDeployableDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
abstract class ComplexDeployable(cdef: ComplexDeployableDefinition) extends PlanetSideServerObject with Deployable {
private var shields: Int = 0
def Shields: Int = shields
def Shields_=(toShields: Int): Int = {
shields = math.min(math.max(0, toShields), MaxShields)
Shields
}
def MaxShields: Int = {
0 //Definition.MaxShields
}
def Definition = cdef
}

View file

@ -3,6 +3,7 @@ package net.psforever.objects.ce
import net.psforever.objects._
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -10,9 +11,25 @@ import net.psforever.objects.zones.ZoneAware
import net.psforever.packet.game.DeployableIcon
import net.psforever.types.PlanetSideEmpire
trait Deployable extends FactionAffinity with Vitality with OwnableByPlayer with ZoneAware {
this: PlanetSideGameObject =>
trait BaseDeployable
extends PlanetSideServerObject
with FactionAffinity
with Vitality
with OwnableByPlayer
with ZoneAware {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var shields: Int = 0
def Shields: Int = shields
def Shields_=(toShields: Int): Int = {
shields = math.min(math.max(0, toShields), MaxShields)
Shields
}
def MaxShields: Int = {
0 //Definition.MaxShields
}
def MaxHealth: Int
@ -28,7 +45,33 @@ trait Deployable extends FactionAffinity with Vitality with OwnableByPlayer with
def Definition: DeployableDefinition
}
abstract class Deployable(cdef: DeployableDefinition)
extends BaseDeployable {
def Definition: DeployableDefinition = cdef
}
object Deployable {
import scala.concurrent.duration._
final val decay: FiniteDuration = 3.minutes
final val cleanup: FiniteDuration = 2.seconds
final case class Deconstruct(time: Option[FiniteDuration] = None)
object Deconstruct {
def apply(): Deconstruct = Deconstruct(None)
}
/**
* Change a vehicle's internal ownership property to match that of the target player.
* @param player the person who will own the vehicle, or `None` if the vehicle will go unowned
*/
final case class Ownership(player: Option[Player])
object Ownership {
def apply(player: Player): Ownership = Ownership(Some(player))
}
object Category {
def Of(item: DeployedItem.Value): DeployableCategory.Value = deployablesToCategories(item)

View file

@ -0,0 +1,316 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.ce
import akka.actor.{Actor, ActorRef, Cancellable}
import net.psforever.objects.guid.GUIDTask
import net.psforever.objects._
import net.psforever.objects.definition.DeployAnimation
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.PlanetSideEmpire
import scala.concurrent.duration._
/**
* A `trait` mixin to manage the basic lifecycle of `Deployable` entities.<br>
* <br>
* Two parts of the deployable lifecycle are supported - building/deployment and dismissal/deconstruction.
* Furthermore, both parts of the lifecycle can also be broken down into two parts for the purposes of sequencing.
* The former part can be referred to as "preparation" which, at the least, queues the future part.
* This latter part can be referred to as "execution" where the the actual process takes place.
* Internal messaging protocol permits the lifecycle to transition.
* "Building" of the deployable starts when a `Setup` request is received during the appropriate window of opportunity
* and queues up a the formal construction event and its packets for a later period (usually a few seconds).
* After being constructed, the deployable can be deconstructed by receiving such a `Deconstruct` message.
* As deployables are capable of being owned by the player,
* in between two two states of being created and deconstructed,
* deployables may also recognize that their ownership has been changed and go through appropriate element shuffling.
* That recognition is much easier before having their construction finalized, however.<br>
* <br>
* Interaction with the major zone deployable management service is crucial.
* @see `OwnableByPlayer`
* @see `ZoneDeployableActor`
*/
trait DeployableBehavior {
_: Actor =>
def DeployableObject: Deployable
/** the timer for the construction process */
var setup: Cancellable = Default.Cancellable
/** the timer for the deconstruction process */
var decay: Cancellable = Default.Cancellable
/** used to manage the internal knowledge of the construction process;
* `None` means "never constructed",
* `Some(false)` means "deconstructed" or a state that is unresponsive to certain messaging input,
* `Some(true)` means "constructed" */
private var constructed: Option[Boolean] = None
/** a value that is utilized by the `ObjectDeleteMessage` packet, affecting animation */
var deletionType: Int = 2
def deployableBehaviorPostStop(): Unit = {
setup.cancel()
decay.cancel()
}
def isConstructed: Option[Boolean] = constructed
val deployableBehavior: Receive = {
case Zone.Deployable.Setup()
if constructed.isEmpty && setup.isCancelled =>
setupDeployable(sender())
case DeployableBehavior.Finalize(callback) =>
finalizeDeployable(callback)
case Deployable.Ownership(None)
if DeployableObject.Owner.nonEmpty =>
val obj = DeployableObject
if (constructed.contains(true)) {
loseOwnership(obj.Faction)
} else {
obj.Owner = None
}
case Deployable.Ownership(Some(player))
if !DeployableObject.Destroyed && DeployableObject.Owner.isEmpty =>
if (constructed.contains(true)) {
gainOwnership(player)
} else {
DeployableObject.AssignOwnership(player)
}
case Deployable.Deconstruct(time)
if constructed.contains(true) =>
deconstructDeployable(time)
case DeployableBehavior.FinalizeElimination() =>
dismissDeployable()
case Zone.Deployable.IsDismissed(obj)
if (obj eq DeployableObject) && (constructed.isEmpty || constructed.contains(false)) =>
unregisterDeployable(obj)
}
/**
* Losing ownership involves updating map screen UI, to remove management rights from the now-previous owner,
* and may involve concealing the deployable from the map screen for the entirety of the previous owner's faction.
* Displaying the deployable on the map screen of another faction may be required.
* @param toFaction the faction to which to set the deployable to be visualized on the map and in the game world;
* may also affect deployable operation
*/
def loseOwnership(toFaction: PlanetSideEmpire.Value): Unit = {
val obj = DeployableObject
val guid = obj.GUID
val zone = obj.Zone
val localEvents = zone.LocalEvents
val originalFaction = obj.Faction
val changeFaction = originalFaction != toFaction
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, Service.defaultPlayerGUID)
if (changeFaction) {
obj.Faction = toFaction
//visual tells in regards to ownership by faction
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
)
//remove knowledge by the previous owner's faction
localEvents ! LocalServiceMessage(
originalFaction.toString,
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
)
//display to the given faction
localEvents ! LocalServiceMessage(
toFaction.toString,
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
)
}
startOwnerlessDecay()
}
def startOwnerlessDecay(): Unit = {
val obj = DeployableObject
if (obj.Owner.nonEmpty && decay.isCancelled) {
//without an owner, this deployable should begin to decay and will deconstruct later
import scala.concurrent.ExecutionContext.Implicits.global
decay = context.system.scheduler.scheduleOnce(Deployable.decay, self, Deployable.Deconstruct())
}
obj.Owner = None //OwnerName should remain set
}
/**
* na
* @see `gainOwnership(Player, PlanetSideEmpire.Value)`
* @param player the player being given ownership of this deployable
*/
def gainOwnership(player: Player): Unit = {
gainOwnership(player, player.Faction)
}
/**
* na
* @param player the player being given ownership of this deployable
* @param toFaction the faction to which the deployable is being assigned;
* usually matches the `player`'s own faction
*/
def gainOwnership(player: Player, toFaction: PlanetSideEmpire.Value): Unit = {
val obj = DeployableObject
obj.AssignOwnership(player)
decay.cancel()
val guid = obj.GUID
val zone = obj.Zone
val originalFaction = obj.Faction
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, obj.Owner.get)
if (originalFaction != toFaction) {
obj.Faction = toFaction
val localEvents = zone.LocalEvents
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
)
localEvents ! LocalServiceMessage(
originalFaction.toString,
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
)
localEvents ! LocalServiceMessage(
toFaction.toString,
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
)
}
}
/**
* The first stage of the deployable build process, to put the formal process in motion.
* Deployables, upon construction, may display an animation effect.
* Parameters are required to be passed onto the next stage of the build process and are not used here.
* @see `DeployableDefinition.deployAnimation`
* @see `DeployableDefinition.DeployTime`
* @see `LocalAction.TriggerEffectLocation`
* @param callback an `ActorRef` used for confirming the deployable's completion of the process
*/
def setupDeployable(callback: ActorRef): Unit = {
val obj = DeployableObject
constructed = Some(false)
if (obj.Definition.deployAnimation == DeployAnimation.Standard) {
val zone = obj.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffectLocation(
obj.Owner.getOrElse(Service.defaultPlayerGUID),
"spawn_object_effect",
obj.Position,
obj.Orientation
)
)
}
import scala.concurrent.ExecutionContext.Implicits.global
setup = context.system.scheduler.scheduleOnce(
obj.Definition.DeployTime milliseconds,
self,
DeployableBehavior.Finalize(callback)
)
}
/**
* The second stage of the deployable build process, to complete the formal process.
* If no owner is assigned, the deployable must immediately begin suffering decay.
* Nothing dangerous happens if it does not begin to decay, but, because it is not under a player's management,
* the deployable will not properly transition to a decay state for another reason
* and can linger in the zone ownerless for as long as it is not destroyed.
* @see `AvatarAction.DeployItem`
* @see `DeploymentAction`
* @see `DeployableInfo`
* @see `LocalAction.DeployableMapIcon`
* @see `Zone.Deployable.IsBuilt`
* @param callback an `ActorRef` used for confirming the deployable's completion of the process
*/
def finalizeDeployable(callback: ActorRef): Unit = {
setup.cancel()
constructed = Some(true)
val obj = DeployableObject
val zone = obj.Zone
val localEvents = zone.LocalEvents
val owner = obj.Owner.getOrElse(Service.defaultPlayerGUID)
obj.OwnerName match {
case Some(_) =>
case None =>
import scala.concurrent.ExecutionContext.Implicits.global
decay = context.system.scheduler.scheduleOnce(
Deployable.decay,
self,
Deployable.Deconstruct()
)
}
//zone build
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.DeployItem(Service.defaultPlayerGUID, obj))
//zone map icon
localEvents ! LocalServiceMessage(
obj.Faction.toString,
LocalAction.DeployableMapIcon(
Service.defaultPlayerGUID,
DeploymentAction.Build, DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, owner)
)
)
//local build management
callback ! Zone.Deployable.IsBuilt(obj)
}
/**
* The first stage of the deployable dismissal process, to put the formal process in motion.
* @param time an optional duration that overrides the deployable's usual duration
*/
def deconstructDeployable(time: Option[FiniteDuration]): Unit = {
constructed = Some(false)
val duration = time.getOrElse(Deployable.cleanup)
import scala.concurrent.ExecutionContext.Implicits.global
setup.cancel()
decay.cancel()
decay = context.system.scheduler.scheduleOnce(duration, self, DeployableBehavior.FinalizeElimination())
}
/**
* The task for unregistering this deployable.
* Most deployables are monolithic entities, requiring only a single unique identifier.
* @param obj the deployable
*/
def unregisterDeployable(obj: Deployable): Unit = {
val zone = obj.Zone
zone.tasks ! GUIDTask.UnregisterObjectTask(obj)(zone.GUID)
}
/**
* The second stage of the deployable build process, to complete the formal process.
* Queue up final deployable unregistering, separate from the zone's deployable governance,
* and instruct all clients in this zone that the deployable is to be deconstructed.
*/
def dismissDeployable(): Unit = {
setup.cancel()
decay.cancel()
val obj = DeployableObject
val zone = obj.Zone
//there's no special meaning behind directing any replies from from zone governance straight back to zone governance
//this deployable control agency, however, will be expiring and can not be a recipient
zone.Deployables ! Zone.Deployable.Dismiss(obj)
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.EliminateDeployable(obj, obj.GUID, obj.Position, deletionType)
)
//when the deployable is being destroyed, certain functions will already have been invoked
//as destruction will instigate deconstruction, skip these invocations to avoid needless message passing
if (!obj.Destroyed) {
Deployables.AnnounceDestroyDeployable(obj)
}
obj.OwnerName = None
}
}
object DeployableBehavior {
/** internal message for progressing the build process */
private case class Finalize(callback: ActorRef)
/** internal message for progresisng the deconstruction process */
private case class FinalizeElimination()
}

View file

@ -1,11 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.SimpleDeployableDefinition
abstract class SimpleDeployable(cdef: SimpleDeployableDefinition) extends PlanetSideGameObject with Deployable {
Health = Definition.MaxHealth
def Definition = cdef
}

View file

@ -1,12 +1,15 @@
// Copyright (c) 2017 PSForever
// Copyright (c) 2018 PSForever
package net.psforever.objects.ce
import akka.actor.ActorContext
import net.psforever.objects.{Default, PlanetSideGameObject, TelepadDeployable, Vehicle}
import akka.actor.{ActorContext, Cancellable}
import net.psforever.objects.{Default, TelepadDeployable, Vehicle}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.vehicles.Utility
import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{GenericObjectActionMessage, ObjectCreateMessage, ObjectDeleteMessage}
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.PlanetSideGUID
trait TelepadLike {
@ -38,9 +41,13 @@ trait TelepadLike {
}
object TelepadLike {
final case class Activate(obj: PlanetSideGameObject with TelepadLike)
final case class RequestLink(obj: TelepadDeployable)
final case class Deactivate(obj: PlanetSideGameObject with TelepadLike)
final case class SeverLink(obj: PlanetSideServerObject with TelepadLike)
final case class Activate(obj: PlanetSideServerObject with TelepadLike)
final case class Deactivate(obj: PlanetSideServerObject with TelepadLike)
/**
* Assemble some logic for a provided object.
@ -64,12 +71,12 @@ object TelepadLike {
* @param zone where the router is located
* @return the pair of units that compose the teleportation system
*/
def AppraiseTeleportationSystem(router: Vehicle, zone: Zone): Option[(Utility.InternalTelepad, TelepadDeployable)] = {
def AppraiseTeleportationSystem(router: Vehicle, zone: Zone): Option[(InternalTelepad, TelepadDeployable)] = {
import net.psforever.objects.vehicles.UtilityType
import net.psforever.types.DriveState
router.Utility(UtilityType.internal_router_telepad_deployable) match {
//if the vehicle has an internal telepad, it is allowed to be a Router (that's a weird way of saying it)
case Some(util: Utility.InternalTelepad) =>
case Some(util: InternalTelepad) =>
//check for a readied remote telepad
zone.GUID(util.Telepad) match {
case Some(telepad: TelepadDeployable) =>
@ -86,6 +93,60 @@ object TelepadLike {
None
}
}
/**
* Create the mechanism that serves as one endpoint of the linked router teleportation system.<br>
* <br>
* Technically, the mechanism - an `InternalTelepad` object - is always made to exist
* due to how the Router vehicle object is encoded into an `ObjectCreateMessage` packet.
* Regardless, that internal mechanism is created anew each time the system links a new remote telepad.
* @param routerGUID the vehicle that houses one end of the teleportation system (the `internalTelepad`)
* @param obj the endpoint of the teleportation system housed by the router
*/
def StartRouterInternalTelepad(zone: Zone, routerGUID: PlanetSideGUID, obj: InternalTelepad): Unit = {
val utilityGUID = obj.GUID
val udef = obj.Definition
val events = zone.LocalEvents
val zoneId = zone.id
/*
the following instantiation and configuration creates the internal Router component
normally dispatched while the Router is transitioned into its Deploying state
it is safe, however, to perform these actions at any time during and after the Deploying state
*/
events ! LocalServiceMessage(
zoneId,
LocalAction.SendResponse(
ObjectCreateMessage(
udef.ObjectId,
utilityGUID,
ObjectCreateMessageParent(routerGUID, 2), //TODO stop assuming slot number
udef.Packet.ConstructorData(obj).get
)
)
)
events ! LocalServiceMessage(
zoneId,
LocalAction.SendResponse(GenericObjectActionMessage(utilityGUID, 27))
)
events ! LocalServiceMessage(
zoneId,
LocalAction.SendResponse(GenericObjectActionMessage(utilityGUID, 30))
)
LinkTelepad(zone, utilityGUID)
}
def LinkTelepad(zone: Zone, telepadGUID: PlanetSideGUID): Unit = {
val events = zone.LocalEvents
val zoneId = zone.id
events ! LocalServiceMessage(
zoneId,
LocalAction.SendResponse(GenericObjectActionMessage(telepadGUID, 27))
)
events ! LocalServiceMessage(
zoneId,
LocalAction.SendResponse(GenericObjectActionMessage(telepadGUID, 28))
)
}
}
/**
@ -95,8 +156,49 @@ object TelepadLike {
* a placeholder like this is easy to reason around.
* @param obj an entity that extends `TelepadLike`
*/
class TelepadControl(obj: TelepadLike) extends akka.actor.Actor {
class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
var setup: Cancellable = Default.Cancellable
def receive: akka.actor.Actor.Receive = {
case TelepadLike.Activate(o: InternalTelepad) if obj eq o =>
obj.Active = true
case TelepadLike.Deactivate(o: InternalTelepad) if obj eq o =>
obj.Active = false
val zone = obj.Zone
zone.GUID(obj.Telepad) match {
case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled =>
oldTpad.Actor ! TelepadLike.SeverLink(obj)
case None => ;
}
obj.Telepad = None
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SendResponse(ObjectDeleteMessage(obj.GUID, 0)))
case TelepadLike.RequestLink(tpad: TelepadDeployable) =>
val zone = obj.Zone
if (obj.Active) {
zone.GUID(obj.Telepad) match {
case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled =>
oldTpad.Actor ! TelepadLike.SeverLink(obj)
case None => ;
}
obj.Telepad = tpad.GUID
//zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.StartRouterInternalTelepad(obj.Owner.GUID, obj.GUID, obj))
TelepadLike.StartRouterInternalTelepad(zone, obj.Owner.GUID, obj)
tpad.Actor ! TelepadLike.Activate(obj)
} else {
val channel = obj.Owner.asInstanceOf[Vehicle].OwnerName.getOrElse("")
zone.LocalEvents ! LocalServiceMessage(channel, LocalAction.RouterTelepadMessage("@Teleport_NotDeployed"))
tpad.Actor ! TelepadLike.SeverLink(obj)
}
case TelepadLike.SeverLink(tpad: TelepadDeployable) =>
if (obj.Telepad.contains(tpad.GUID)) {
obj.Telepad = None
val zone = obj.Zone
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SendResponse(ObjectDeleteMessage(obj.GUID, 0)))
}
case _ => ;
}
}

View file

@ -2,10 +2,9 @@
package net.psforever.objects.definition
import akka.actor.ActorContext
import net.psforever.objects.{Default, PlanetSideGameObject}
import net.psforever.objects.Default
import net.psforever.objects.ce.{Deployable, DeployableCategory, DeployedItem}
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -13,9 +12,16 @@ import net.psforever.objects.vital.{NoResistanceSelection, VitalityDefinition}
import scala.concurrent.duration._
object DeployAnimation extends Enumeration {
type Type = Value
val None, Standard, Fdu = Value
}
trait BaseDeployableDefinition {
private var category: DeployableCategory.Value = DeployableCategory.Boomers
private var deployTime: Long = (1 second).toMillis //ms
var deployAnimation: DeployAnimation.Value = DeployAnimation.None
def Item: DeployedItem.Value
@ -35,13 +41,12 @@ trait BaseDeployableDefinition {
DeployTime
}
def Initialize(obj: PlanetSideGameObject with Deployable, context: ActorContext): Unit = {}
def Initialize(obj: Deployable, context: ActorContext): Unit = {}
def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext): Unit = {}
def Uninitialize(obj: PlanetSideGameObject with Deployable, context: ActorContext): Unit = {}
def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext): Unit = {}
def Uninitialize(obj: Deployable, context: ActorContext): Unit = {
obj.Actor ! akka.actor.PoisonPill
obj.Actor = Default.Actor
}
}
abstract class DeployableDefinition(objectId: Int)
@ -53,24 +58,7 @@ abstract class DeployableDefinition(objectId: Int)
private val item = DeployedItem(objectId) //let throw NoSuchElementException
DamageUsing = DamageCalculations.AgainstVehicle
ResistUsing = NoResistanceSelection
Packet = new SmallDeployableConverter
def Item: DeployedItem.Value = item
}
class SimpleDeployableDefinition(objectId: Int) extends DeployableDefinition(objectId) {
Packet = new SmallDeployableConverter
}
abstract class ComplexDeployableDefinition(objectId: Int) extends DeployableDefinition(objectId)
object SimpleDeployableDefinition {
def apply(item: DeployedItem.Value): SimpleDeployableDefinition =
new SimpleDeployableDefinition(item.id)
def SimpleUninitialize(obj: PlanetSideGameObject, context: ActorContext): Unit = {}
def SimpleUninitialize(obj: PlanetSideServerObject, context: ActorContext): Unit = {
context.stop(obj.Actor)
obj.Actor = Default.Actor
}
}

View file

@ -2,15 +2,14 @@
package net.psforever.objects.definition.converter
import net.psforever.objects.ce.Deployable
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.equipment.JammableUnit
import net.psforever.packet.game.objectcreate._
import net.psforever.types.PlanetSideGUID
import scala.util.{Failure, Success, Try}
class SmallDeployableConverter extends ObjectCreateConverter[PlanetSideGameObject with Deployable]() {
override def ConstructorData(obj: PlanetSideGameObject with Deployable): Try[CommonFieldDataWithPlacement] = {
class SmallDeployableConverter extends ObjectCreateConverter[Deployable]() {
override def ConstructorData(obj: Deployable): Try[CommonFieldDataWithPlacement] = {
Success(
CommonFieldDataWithPlacement(
PlacementData(obj.Position, obj.Orientation),
@ -35,6 +34,6 @@ class SmallDeployableConverter extends ObjectCreateConverter[PlanetSideGameObjec
)
}
override def DetailedConstructorData(obj: PlanetSideGameObject with Deployable): Try[CommonFieldDataWithPlacement] =
override def DetailedConstructorData(obj: Deployable): Try[CommonFieldDataWithPlacement] =
Failure(new Exception("converter should not be used to generate detailed small deployable data"))
}

View file

@ -1,8 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.Vehicle
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.{PlanetSideGameObject, Vehicle}
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{DriveState, PlanetSideGUID}
@ -86,7 +86,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
private def MakeMountings(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
obj.Weapons.collect {
case (index, slot) if slot.Equipment.nonEmpty =>
case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty =>
val equip: Equipment = slot.Equipment.get
val equipDef = equip.Definition
InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get)
@ -98,8 +98,8 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
.EquipmentUtilities(obj.Utilities)
.map({
case (index, utilContainer) =>
val util = utilContainer()
val utilDef = util.Definition
val util: PlanetSideGameObject = utilContainer()
val utilDef = util.Definition
InventoryItemData(utilDef.ObjectId, util.GUID, index, utilDef.Packet.ConstructorData(util).get)
})
.toList

View file

@ -7,7 +7,19 @@ package net.psforever.objects.equipment
* All weapons and some support items have fire modes, though most only have one.
* The number of fire modes is visually indicated by the bubbles next to the icon of the `Equipment` in a holster slot.
* The specifics of how a fire mode affects the output is left to implementation and execution.
* Contrast how `Tool`s deal with multiple types of ammunition.
* Contrast how `Tool`s deal with multiple types of ammunition.<br>
* <br>
* While most `Tools` - weapons and such - are known to have fire modes,
* `ConstructionItem` equipment that produce deployable entities in the game world also support fire modes.
* The mechanism is different, however, even while the user interactions work in a similar way.
* For most weapons, the fire mode is just a modification of how the projectiles behave or the weapon behaves.
* For example, the bounciness of the grenades or the number of shots fired by the Jackhammer.
* One has to change tool ammo types to actual swap out different ammunitions such as, most commonly,
* grey normal ammo for gold armor-piercing ammo.
* For deployable-constructing entities, fire mode switches between the categories of deployables that can be built
* and "changing ammunition" actually changes the subtype of deployable within that deployable category.
* For example, fire modes go from "Boomers" to "Mines"
* while ammo types for "Mines" goes from "HE mines" to "Disruptor Mines".
* @tparam Mode the type parameter representing the fire mode
*/
trait FireModeSwitch[Mode] {

View file

@ -31,7 +31,6 @@ import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.types._
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global
@ -605,17 +604,9 @@ class VehicleControl(vehicle: Vehicle)
state match {
case DriveState.Deploying =>
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) =>
util.Active = true
case _ =>
//log.warn(s"DeploymentActivities: could not find internal telepad in router@${vehicle.GUID.guid} while $state")
case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Activate(util)
case _ => ;
}
case DriveState.Deployed =>
//let the timer do all the work
events ! LocalServiceMessage(
zoneChannel,
LocalAction.ToggleTeleportSystem(GUID0, vehicle, TelepadLike.AppraiseTeleportationSystem(vehicle, zone))
)
case _ => ;
}
}
@ -663,8 +654,10 @@ class VehicleControl(vehicle: Vehicle)
state match {
case DriveState.Undeploying =>
//deactivate internal router before trying to reset the system
Vehicles.RemoveTelepads(vehicle)
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.ToggleTeleportSystem(GUID0, vehicle, None))
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Deactivate(util)
case _ => ;
}
case _ => ;
}
}

View file

@ -2,7 +2,6 @@
package net.psforever.objects.vital
import net.psforever.objects.vital.resolution.{DamageAndResistance, ResolutionCalculations}
import net.psforever.objects.vital.interaction.DamageResult
/**
* A vital object can be hurt or damaged or healed or repaired (HDHR).
@ -54,12 +53,4 @@ object Vitality {
* @param func a function literal
*/
final case class Damage(func: ResolutionCalculations.Output)
final case class DamageOn(obj: Vitality, func: ResolutionCalculations.Output)
/**
* Report that a vitals object must be updated due to damage.
* @param obj the vital object
*/
final case class DamageResolution(obj: Vitality, cause: DamageResult)
}

View file

@ -3,7 +3,7 @@ package net.psforever.objects.vital.resolution
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.ce.ComplexDeployable
import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.vital.base.DamageResolution
@ -266,7 +266,7 @@ object ResolutionCalculations {
ce.Health -= damage
}
case ce: ComplexDeployable if CanDamage(ce, damage, data) =>
case ce: Deployable if CanDamage(ce, damage, data) =>
if (ce.Shields > 0) {
if (damage > ce.Shields) {
ce.Health -= (damage - ce.Shields)
@ -310,7 +310,7 @@ object ResolutionCalculations {
}
VehicleApplication(dam, data)(target)
case _: ComplexDeployable =>
case _: Deployable =>
val dam : Int = damage match {
case a: Int => a
case _ => 0

View file

@ -3,9 +3,9 @@ package net.psforever.objects.zones
import akka.actor.{ActorContext, ActorRef, Props}
import akka.routing.RandomPool
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.{PlanetSideGameObject, _}
import net.psforever.objects.ce.{ComplexDeployable, Deployable, SimpleDeployable}
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.{NumberPoolHub, TaskResolver}
@ -101,7 +101,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
/**
*/
private val constructions: ListBuffer[PlanetSideGameObject with Deployable] = ListBuffer()
private val constructions: ListBuffer[Deployable] = ListBuffer()
/**
*/
@ -536,7 +536,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
*/
def EquipmentOnGround: List[Equipment] = equipmentOnGround.toList
def DeployableList: List[PlanetSideGameObject with Deployable] = constructions.toList
def DeployableList: List[Deployable] = constructions.toList
def Vehicles: List[Vehicle] = vehicles.toList
@ -930,11 +930,14 @@ object Zone {
}
object Deployable {
final case class Build(obj: PlanetSideGameObject with Deployable, withTool: ConstructionItem)
final case class DeployableIsBuilt(obj: PlanetSideGameObject with Deployable, withTool: ConstructionItem)
final case class Build(obj: Deployable)
final case class BuildByOwner(obj: Deployable, owner: Player, withTool: ConstructionItem)
final case class Setup()
final case class IsBuilt(obj: Deployable)
final case class CanNotBeBuilt(obj: Deployable, withTool: ConstructionItem)
final case class Dismiss(obj: PlanetSideGameObject with Deployable)
final case class DeployableIsDismissed(obj: PlanetSideGameObject with Deployable)
final case class Dismiss(obj: Deployable)
final case class IsDismissed(obj: Deployable)
}
object Vehicle {
@ -1109,7 +1112,7 @@ object Zone {
source: PlanetSideGameObject with FactionAffinity with Vitality,
createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction,
testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck,
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality]) = findAllTargets
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality] = findAllTargets
): List[PlanetSideServerObject] = {
source.Definition.innateDamage match {
case Some(damage) =>
@ -1145,20 +1148,16 @@ object Zone {
properties: DamageWithPosition,
createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction,
testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean,
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality])
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject] = {
//collect targets that can be damaged
val (pssos, psgos) = acquireTargetsFromZone(zone, source, properties)
val pssos = acquireTargetsFromZone(zone, source, properties)
val radius = properties.DamageRadius * properties.DamageRadius
//restrict to targets according to the detection plan
val allAffectedTargets = pssos.filter { target => testTargetsFromZone(source, target, radius) }
//inform remaining targets that they have suffered damage
allAffectedTargets
.foreach { target => target.Actor ! Vitality.Damage(createInteraction(source, target).calculate()) }
//important note - these are not returned as targets that were affected
psgos
.filter { target => testTargetsFromZone(source, target, radius) }
.foreach { target => zone.LocalEvents ! Vitality.DamageOn(target, createInteraction(source, target).calculate()) }
allAffectedTargets
}
@ -1172,18 +1171,16 @@ object Zone {
* @see `Zone.DeployableList`
* @see `Zone.LivePlayers`
* @see `Zone.Vehicles`
* @param zone the zone in which to search
* @param zone the zone in which the explosion should occur
* @param source a game entity that is treated as the origin and is excluded from results
* @param damagePropertiesBySource information about the effect/damage
* @return two lists of objects with different characteristics;
* the first list is `PlanetSideServerObject` entities with `Vitality`;
* the second list is `PlanetSideGameObject` entities with both `Vitality` and `FactionAffinity`
* @return a list of affected entities
*/
def findAllTargets(
zone: Zone,
source: PlanetSideGameObject with Vitality,
damagePropertiesBySource: DamageWithPosition
): (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality]) = {
): List[PlanetSideServerObject with Vitality] = {
val sourcePosition = source.Position
val sourcePositionXY = sourcePosition.xy
val radius = damagePropertiesBySource.DamageRadius * damagePropertiesBySource.DamageRadius
@ -1193,16 +1190,7 @@ object Zone {
//vehicles
val vehicleTargets = zone.Vehicles.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
//deployables
val (simpleDeployableTargets, complexDeployableTargets) =
zone.DeployableList
.filterNot { _.Destroyed }
.foldRight((List.empty[SimpleDeployable], List.empty[ComplexDeployable])) { case (f, (simp, comp)) =>
f match {
case o: SimpleDeployable => (simp :+ o, comp)
case o: ComplexDeployable => (simp, comp :+ o)
case _ => (simp, comp)
}
}
val deployableTargets = zone.DeployableList.filterNot { _.Destroyed }
//amenities
val soiTargets = source match {
case o: Amenity =>
@ -1217,12 +1205,9 @@ object Zone {
.flatMap { _.Amenities }
.filter { _.Definition.Damageable }
}
(
(playerTargets ++ vehicleTargets ++ complexDeployableTargets ++ soiTargets)
.filter { target => target ne source },
simpleDeployableTargets
.filter { target => target ne source }
)
//restrict to targets according to the detection plan
(playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets).filter { target => target ne source }
}
/**

View file

@ -2,9 +2,8 @@
package net.psforever.objects.zones
import akka.actor.Actor
import net.psforever.objects.Player
import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.PlanetSideGameObject
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
@ -13,46 +12,51 @@ import scala.collection.mutable.ListBuffer
* na
* @param zone the `Zone` object
*/
class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[PlanetSideGameObject with Deployable]) extends Actor {
class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) extends Actor {
import ZoneDeployableActor._
private[this] val log = org.log4s.getLogger
def receive: Receive = {
case Zone.Deployable.Build(obj, tool) =>
case Zone.Deployable.Build(obj) =>
if (DeployableBuild(obj, deployableList)) {
obj match {
case o: PlanetSideServerObject =>
obj.Definition.Initialize(o, context)
case _ =>
obj.Definition.Initialize(obj, context)
}
obj.Zone = zone
sender() ! Zone.Deployable.DeployableIsBuilt(obj, tool)
obj.Definition.Initialize(obj, context)
obj.Actor ! Zone.Deployable.Setup()
} else {
log.warn(s"failed to build deployable ${obj} ${tool}")
log.warn(s"failed to build a ${obj.Definition.Name}")
sender() ! Zone.Deployable.IsDismissed(obj)
}
case Zone.Deployable.BuildByOwner(obj, owner, tool) =>
if (DeployableBuild(obj, deployableList)) {
obj.Zone = zone
obj.Definition.Initialize(obj, context)
owner.Actor ! Player.BuildDeployable(obj, tool)
} else {
log.warn(s"failed to build a ${obj.Definition.Name} belonging to ${obj.OwnerName.getOrElse("no one")}")
sender() ! Zone.Deployable.IsDismissed(obj)
}
case Zone.Deployable.Dismiss(obj) =>
if (DeployableDismiss(obj, deployableList)) {
obj match {
case o: PlanetSideServerObject =>
obj.Definition.Uninitialize(o, context)
case _ =>
obj.Definition.Uninitialize(obj, context)
}
sender() ! Zone.Deployable.DeployableIsDismissed(obj)
obj.Actor ! Zone.Deployable.IsDismissed(obj)
obj.Definition.Uninitialize(obj, context)
}
case Zone.Deployable.IsBuilt(_) => ;
case Zone.Deployable.IsDismissed(_) => ;
case _ => ;
}
}
object ZoneDeployableActor {
def DeployableBuild(
obj: PlanetSideGameObject with Deployable,
deployableList: ListBuffer[PlanetSideGameObject with Deployable]
obj: Deployable,
deployableList: ListBuffer[Deployable]
): Boolean = {
deployableList.find(d => d == obj) match {
case Some(_) =>
@ -64,8 +68,8 @@ object ZoneDeployableActor {
}
def DeployableDismiss(
obj: PlanetSideGameObject with Deployable,
deployableList: ListBuffer[PlanetSideGameObject with Deployable]
obj: Deployable,
deployableList: ListBuffer[Deployable]
): Boolean = {
recursiveFindDeployable(deployableList.iterator, obj) match {
case None =>
@ -77,8 +81,8 @@ object ZoneDeployableActor {
}
@tailrec final def recursiveFindDeployable(
iter: Iterator[PlanetSideGameObject with Deployable],
target: PlanetSideGameObject with Deployable,
iter: Iterator[Deployable],
target: Deployable,
index: Int = 0
): Option[Int] = {
if (!iter.hasNext) {

View file

@ -2,7 +2,6 @@ package net.psforever.packet.control
import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket}
import scodec.Codec
import scodec.bits._
import scodec.codecs._
final case class Unknown30(clientNonce: Long) extends PlanetSideControlPacket {
@ -12,7 +11,5 @@ final case class Unknown30(clientNonce: Long) extends PlanetSideControlPacket {
}
object Unknown30 extends Marshallable[Unknown30] {
implicit val codec: Codec[Unknown30] = (
("client_nonce" | uint32L)
).as[Unknown30]
implicit val codec: Codec[Unknown30] = ("client_nonce" | uint32L).as[Unknown30]
}

View file

@ -7,9 +7,17 @@ import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* `DetailedACEData` - `data.faction` is faction affinity, `data.unk1` is `true`
* `DetailedBoomerTriggerData` - `data.faction` can be `NEUTRAL`, `data.unk1` is `true`
* `DetailedTelepadData` - `data.faction` can be `NEUTRAL`, `data.jammered` is the router's GUID
* A representation of the construction item portion of `ObjectCreateDetailedMessage` packet data.
* This creates what is known as a construction tool item (or, `ConstructionItem`)
* which is a game world object that is manipulated by the player
* to construct other game world objects which are known as combat engineering deployables (or, just `Deployable`s).
* None of the information about the `Deployable` objects are maintained here and
* are instead implicit to the type of `ConstructionItem`.
* That aspect of the entity is adjusted through fire modes and ammunition types
* much like conventional weaponry (`Tool`s), though the initial fire mode can be indicated.
* @see `ConstructionItem`
* @see `Deployable`
* @see `FireModeSwitch`
*/
final case class DetailedConstructionToolData(data: CommonFieldData, mode: Int) extends ConstructorData {
override def bitsize: Long = 28L + data.bitsize
@ -20,7 +28,7 @@ object DetailedConstructionToolData extends Marshallable[DetailedConstructionToo
implicit val codec: Codec[DetailedConstructionToolData] = (
("data" | CommonFieldData.codec(false)) ::
uint8 ::
uint8 :: //n > 1 produces a stack of construction items (tends to crash the client)
("mode" | uint16) ::
uint2 ::
uint2

View file

@ -280,7 +280,7 @@ class AvatarService(zone: Zone) extends Actor {
)
case AvatarAction.PutDownFDU(player_guid) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.PutDownFDU(player_guid))
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.PutDownFDU(player_guid))
)
case AvatarAction.Release(player, _, time) =>
undertaker forward RemoverActor.AddTask(player, zone, time)

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.services.avatar
import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.Player
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
@ -40,7 +40,7 @@ object AvatarAction {
final case class ConcealPlayer(player_guid: PlanetSideGUID) extends Action
final case class EnvironmentalDamage(player_guid: PlanetSideGUID, source_guid: PlanetSideGUID, amount: Int)
extends Action
final case class DeployItem(player_guid: PlanetSideGUID, item: PlanetSideGameObject with Deployable) extends Action
final case class DeployItem(player_guid: PlanetSideGUID, item: Deployable) extends Action
final case class DeactivateImplantSlot(player_guid: PlanetSideGUID, slot: Int) extends Action
final case class ActivateImplantSlot(player_guid: PlanetSideGUID, slot: Int) extends Action
final case class Destroy(victim: PlanetSideGUID, killer: PlanetSideGUID, weapon: PlanetSideGUID, pos: Vector3)

View file

@ -3,7 +3,7 @@ package net.psforever.services.galaxy
import akka.actor.Actor
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{BuildingInfoUpdateMessage, FlagInfo}
import net.psforever.packet.game.BuildingInfoUpdateMessage
import net.psforever.services.{GenericEventBus, Service}
class GalaxyService extends Actor {

View file

@ -3,7 +3,7 @@ package net.psforever.services.galaxy
import net.psforever.objects.Vehicle
import net.psforever.objects.vehicles.VehicleManifest
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage, FlagInfo}
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
import net.psforever.types.PlanetSideGUID
final case class GalaxyServiceMessage(forChannel: String, actionMessage: GalaxyAction.Action)

View file

@ -4,7 +4,7 @@ package net.psforever.services.galaxy
import net.psforever.objects.Vehicle
import net.psforever.objects.vehicles.VehicleManifest
import net.psforever.objects.zones.HotSpotInfo
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage, FlagInfo}
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
import net.psforever.types.PlanetSideGUID
import net.psforever.services.GenericEventBusMsg

View file

@ -1,33 +1,29 @@
// Copyright (c) 2017 PSForever
package net.psforever.services.local
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.objects._
import net.psforever.objects.ce.Deployable
import akka.actor.{Actor, Props}
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.vehicles.{Utility, UtilityType}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{TriggeredEffect, TriggeredEffectLocation}
import net.psforever.services.local.support.{CaptureFlagManager, _}
import net.psforever.services.local.support.CaptureFlagManager
import net.psforever.types.PlanetSideGUID
import net.psforever.services.local.support._
import net.psforever.services.{GenericEventBus, Service}
import net.psforever.services.support.SupportActor
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.services.{GenericEventBus, RemoverActor, Service}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.concurrent.duration.{Duration, _}
class LocalService(zone: Zone) extends Actor {
private val doorCloser = context.actorOf(Props[DoorCloseActor](), s"${zone.id}-local-door-closer")
private val hackClearer = context.actorOf(Props[HackClearActor](), s"${zone.id}-local-hack-clearer")
private val hackCapturer =
context.actorOf(Props(classOf[HackCaptureActor], zone.tasks), s"${zone.id}-local-hack-capturer")
private val captureFlagManager =
context.actorOf(Props(classOf[CaptureFlagManager], zone.tasks, zone), s"${zone.id}-local-capture-flag-manager")
private val engineer =
context.actorOf(Props(classOf[DeployableRemover], zone.tasks), s"${zone.id}-deployable-remover-agent")
private val teleportDeployment: ActorRef =
context.actorOf(Props[RouterTelepadActivation](), s"${zone.id}-telepad-activate-agent")
private val doorCloser = context.actorOf(
Props[DoorCloseActor](), s"${zone.id}-local-door-closer"
)
private val hackClearer = context.actorOf(
Props[HackClearActor](), s"${zone.id}-local-hack-clearer"
)
private val hackCapturer = context.actorOf(
Props(classOf[HackCaptureActor], zone.tasks), s"${zone.id}-local-hack-capturer"
)
private val captureFlagManager = context.actorOf(
Props(classOf[CaptureFlagManager], zone.tasks, zone), s"${zone.id}-local-capture-flag-manager"
)
private[this] val log = org.log4s.getLogger
val LocalEvents = new GenericEventBus[LocalServiceResponse]
@ -49,14 +45,6 @@ class LocalService(zone: Zone) extends Actor {
case LocalServiceMessage(forChannel, action) =>
action match {
case LocalAction.AlertDestroyDeployable(_, obj) =>
LocalEvents.publish(
LocalServiceResponse(
s"/$forChannel/Local",
Service.defaultPlayerGUID,
LocalResponse.AlertDestroyDeployable(obj)
)
)
case LocalAction.DeployableMapIcon(player_guid, behavior, deployInfo) =>
LocalEvents.publish(
LocalServiceResponse(
@ -65,6 +53,14 @@ class LocalService(zone: Zone) extends Actor {
LocalResponse.DeployableMapIcon(behavior, deployInfo)
)
)
case LocalAction.DeployableUIFor(item) =>
LocalEvents.publish(
LocalServiceResponse(
s"/$forChannel/Local",
Service.defaultPlayerGUID,
LocalResponse.DeployableUIFor(item)
)
)
case LocalAction.Detonate(guid, obj) =>
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.Detonate(guid, obj))
@ -84,6 +80,14 @@ class LocalService(zone: Zone) extends Actor {
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.DoorCloses(door_guid))
)
case LocalAction.EliminateDeployable(obj, guid, pos, effect) =>
LocalEvents.publish(
LocalServiceResponse(
s"/$forChannel/Local",
Service.defaultPlayerGUID,
LocalResponse.EliminateDeployable(obj, guid, pos, effect)
)
)
case LocalAction.HackClear(player_guid, target, unk1, unk2) =>
LocalEvents.publish(
LocalServiceResponse(
@ -144,7 +148,6 @@ class LocalService(zone: Zone) extends Actor {
LocalResponse.SendPlanetsideAttributeMessage(target_guid, attribute_number, attribute_value)
)
)
case LocalAction.SendGenericObjectActionMessage(player_guid, target_guid, action_number) =>
LocalEvents.publish(
LocalServiceResponse(
@ -171,7 +174,14 @@ class LocalService(zone: Zone) extends Actor {
LocalResponse.SendGenericActionMessage(action_number)
)
)
case LocalAction.RouterTelepadMessage(msg) =>
LocalEvents.publish(
LocalServiceResponse(
s"/$forChannel/Local",
Service.defaultPlayerGUID,
LocalResponse.RouterTelepadMessage(msg)
)
)
case LocalAction.RouterTelepadTransport(player_guid, passenger_guid, src_guid, dest_guid) =>
LocalEvents.publish(
LocalServiceResponse(
@ -224,6 +234,14 @@ class LocalService(zone: Zone) extends Actor {
LocalResponse.ShuttleState(guid, pos, orient, state)
)
)
case LocalAction.StartRouterInternalTelepad(router_guid, obj_guid, obj) =>
LocalEvents.publish(
LocalServiceResponse(
s"/$forChannel/Local",
Service.defaultPlayerGUID,
LocalResponse.StartRouterInternalTelepad(router_guid, obj_guid, obj)
)
)
case LocalAction.ToggleTeleportSystem(player_guid, router, system_plan) =>
LocalEvents.publish(
LocalServiceResponse(
@ -288,13 +306,6 @@ class LocalService(zone: Zone) extends Actor {
//response from HackClearActor
case HackClearActor.SendHackMessageHackCleared(target_guid, _, unk1, unk2) =>
log.info(s"Clearing hack for $target_guid")
LocalEvents.publish(
LocalServiceResponse(
s"/${zone.id}/Local",
Service.defaultPlayerGUID,
LocalResponse.SendHackMessageHackCleared(target_guid, unk1, unk2)
)
)
//message from ProximityTerminalControl
case Terminal.StartProximityEffect(terminal) =>
@ -314,123 +325,6 @@ class LocalService(zone: Zone) extends Actor {
)
)
//message to Engineer
case LocalServiceMessage.Deployables(msg) =>
engineer forward msg
//message(s) from Engineer
case msg @ DeployableRemover.EliminateDeployable(obj: TurretDeployable, guid, pos, _) =>
val seats = obj.Seats.values
if (seats.count(_.isOccupied) > 0) {
val wasKickedByDriver = false //TODO yeah, I don't know
seats.foreach(seat => {
seat.occupant match {
case Some(tplayer) =>
seat.unmount(tplayer)
tplayer.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, obj.GUID)
)
case None => ;
}
})
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(Duration.create(2, "seconds"), self, msg)
} else {
EliminateDeployable(obj, guid, pos)
}
case DeployableRemover.EliminateDeployable(obj: BoomerDeployable, guid, pos, _) =>
EliminateDeployable(obj, guid, pos)
obj.Trigger match {
case Some(trigger) =>
log.warn(s"LocalService: deconstructing boomer in ${zone.id}, but trigger@${trigger.GUID.guid} still exists")
case _ => ;
}
case DeployableRemover.EliminateDeployable(obj: TelepadDeployable, guid, pos, _) =>
obj.Active = false
//ClearSpecific will also remove objects that do not have GUID's; we may not have a GUID at this time
teleportDeployment ! SupportActor.ClearSpecific(List(obj), zone)
EliminateDeployable(obj, guid, pos)
case DeployableRemover.EliminateDeployable(obj, guid, pos, _) =>
EliminateDeployable(obj, guid, pos)
case DeployableRemover.DeleteTrigger(trigger_guid, _) =>
LocalEvents.publish(
LocalServiceResponse(
s"/${zone.id}/Local",
Service.defaultPlayerGUID,
LocalResponse.ObjectDelete(trigger_guid, 0)
)
)
//message to RouterTelepadActivation
case LocalServiceMessage.Telepads(msg) =>
teleportDeployment forward msg
//from RouterTelepadActivation
case RouterTelepadActivation.ActivateTeleportSystem(telepad, _) =>
val remoteTelepad = telepad.asInstanceOf[TelepadDeployable]
remoteTelepad.Active = true
zone.GUID(remoteTelepad.Router) match {
case Some(router: Vehicle) =>
router.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(internalTelepad: Utility.InternalTelepad) =>
//get rid of previous linked remote telepad (if any)
zone.GUID(internalTelepad.Telepad) match {
case Some(old: TelepadDeployable) =>
log.trace(
s"ActivateTeleportSystem: old remote telepad@${old.GUID.guid} linked to internal@${internalTelepad.GUID.guid} will be deconstructed"
)
old.Active = false
engineer ! SupportActor.ClearSpecific(List(old), zone)
engineer ! RemoverActor.AddTask(old, zone, Some(0 seconds))
case _ => ;
}
internalTelepad.Telepad = remoteTelepad.GUID
if (internalTelepad.Active) {
log.trace(
s"ActivateTeleportSystem: fully deployed router@${router.GUID.guid} in ${zone.id} will link internal@${internalTelepad.GUID.guid} and remote@${remoteTelepad.GUID.guid}"
)
LocalEvents.publish(
LocalServiceResponse(
s"/${zone.id}/Local",
Service.defaultPlayerGUID,
LocalResponse.ToggleTeleportSystem(router, Some((internalTelepad, remoteTelepad)))
)
)
} else {
remoteTelepad.OwnerName match {
case Some(name) =>
LocalEvents.publish(
LocalServiceResponse(
s"/$name/Local",
Service.defaultPlayerGUID,
LocalResponse.RouterTelepadMessage("@Teleport_NotDeployed")
)
)
case None => ;
}
}
case _ =>
log.error(s"ActivateTeleportSystem: vehicle@${router.GUID.guid} in ${zone.id} is not a router?")
RouterTelepadError(remoteTelepad, "@Telepad_NoDeploy_RouterLost")
}
case Some(o) =>
log.error(s"ActivateTeleportSystem: ${o.Definition.Name}@${o.GUID.guid} in ${zone.id} is not a router")
RouterTelepadError(remoteTelepad, "@Telepad_NoDeploy_RouterLost")
case None =>
RouterTelepadError(remoteTelepad, "@Telepad_NoDeploy_RouterLost")
}
//synchronized damage calculations
case Vitality.DamageOn(target: PlanetSideGameObject with Deployable, damage_func) =>
val cause = damage_func(target)
sender() ! Vitality.DamageResolution(target, cause)
// Forward all CaptureFlagManager messages
case msg @ (CaptureFlagManager.SpawnCaptureFlag(_, _, _) | CaptureFlagManager.PickupFlag(_, _) |
CaptureFlagManager.DropFlag(_) | CaptureFlagManager.Captured(_) | CaptureFlagManager.Lost(_, _) |
@ -440,53 +334,4 @@ class LocalService(zone: Zone) extends Actor {
case msg =>
log.warn(s"Unhandled message $msg from ${sender()}")
}
/**
* na
* @param telepad na
* @param msg na
*/
def RouterTelepadError(telepad: TelepadDeployable, msg: String): Unit = {
telepad.OwnerName match {
case Some(name) =>
LocalEvents.publish(
LocalServiceResponse(s"/$name/Local", Service.defaultPlayerGUID, LocalResponse.RouterTelepadMessage(msg))
)
case None => ;
}
engineer ! SupportActor.ClearSpecific(List(telepad), zone)
engineer ! RemoverActor.AddTask(telepad, zone, Some(0 seconds))
}
/**
* Common behavior for distributing information about a deployable's destruction or deconstruction.<br>
* <br>
* The primary distribution task instructs all clients to eliminate the target deployable.
* This is a cosmetic exercise as the deployable should already be unregistered from its zone and
* functionally removed from its zone's list of deployable objects by external operations.
* The other distribution is a targeted message sent to the former owner of the deployable
* if he still exists on the server
* to clean up any leftover ownership-specific knowledge about the deployable.
* @see `DeployableRemover`
* @param obj the deployable object
* @param guid the deployable objects globally unique identifier;
* may be a former identifier
* @param position the deployable's position
*/
def EliminateDeployable(obj: PlanetSideGameObject with Deployable, guid: PlanetSideGUID, position: Vector3): Unit = {
LocalEvents.publish(
LocalServiceResponse(
s"/${zone.id}/Local",
Service.defaultPlayerGUID,
LocalResponse.EliminateDeployable(obj, guid, position)
)
)
obj.OwnerName match {
case Some(name) =>
LocalEvents.publish(
LocalServiceResponse(s"/$name/Local", Service.defaultPlayerGUID, LocalResponse.AlertDestroyDeployable(obj))
)
case None => ;
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.services.local
import net.psforever.objects.ce.Deployable
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.hackable.Hackable
@ -22,24 +22,27 @@ final case class LocalServiceMessage(forChannel: String, actionMessage: LocalAct
object LocalServiceMessage {
final case class Deployables(msg: Any)
final case class Telepads(msg: Any)
}
object LocalAction {
trait Action
final case class AlertDestroyDeployable(player_guid: PlanetSideGUID, obj: PlanetSideGameObject with Deployable)
extends Action
final case class DeployableMapIcon(
player_guid: PlanetSideGUID,
behavior: DeploymentAction.Value,
deployInfo: DeployableInfo
) extends Action
player_guid: PlanetSideGUID,
behavior: DeploymentAction.Value,
deployInfo: DeployableInfo
) extends Action
final case class DeployableUIFor(obj: DeployedItem.Value) 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 DoorCloses(player_guid: PlanetSideGUID, door_guid: PlanetSideGUID) extends Action
final case class DoorSlamsShut(door: Door) extends Action
final case class EliminateDeployable(
obj: Deployable,
object_guid: PlanetSideGUID,
pos: Vector3,
deletionEffect: Int
) extends Action
final case class HackClear(player_guid: PlanetSideGUID, target: PlanetSideServerObject, unk1: Long, unk2: Long = 8L)
extends Action
final case class HackTemporarily(
@ -66,7 +69,6 @@ object LocalAction {
attribute_number: PlanetsideAttributeEnum,
attribute_value: Long
) extends Action
final case class SendGenericObjectActionMessage(
player_guid: PlanetSideGUID,
target: PlanetSideGUID,
@ -82,7 +84,7 @@ object LocalAction {
player_guid: PlanetSideGUID,
action_number: GenericActionEnum
) extends Action
final case class RouterTelepadMessage(msg: String) extends Action
final case class RouterTelepadTransport(
player_guid: PlanetSideGUID,
passenger_guid: PlanetSideGUID,
@ -99,6 +101,11 @@ object LocalAction {
) 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 StartRouterInternalTelepad(
router_guid: PlanetSideGUID,
obj_guid: PlanetSideGUID,
obj: Utility.InternalTelepad
) extends Action
final case class ToggleTeleportSystem(
player_guid: PlanetSideGUID,
router: Vehicle,

View file

@ -1,13 +1,11 @@
// Copyright (c) 2017 PSForever
package net.psforever.services.local
import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.serverobject.structures.{AmenityOwner, Building}
import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle}
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.objects.vehicles.Utility
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.GenericActionEnum.GenericActionEnum
import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
@ -26,17 +24,18 @@ final case class LocalServiceResponse(
object LocalResponse {
trait Response
final case class AlertDestroyDeployable(obj: PlanetSideGameObject with Deployable) extends Response
final case class DeployableMapIcon(action: DeploymentAction.Value, deployInfo: DeployableInfo) extends Response
final case class DeployableUIFor(obj: DeployedItem.Value) extends Response
final case class Detonate(guid: PlanetSideGUID, obj: PlanetSideGameObject) extends Response
final case class DoorOpens(door_guid: PlanetSideGUID) extends Response
final case class DoorCloses(door_guid: PlanetSideGUID) extends Response
final case class EliminateDeployable(
obj: PlanetSideGameObject with Deployable,
object_guid: PlanetSideGUID,
pos: Vector3
) extends Response
final case class SendHackMessageHackCleared(target_guid: PlanetSideGUID, unk1: Long, unk2: Long) extends Response
obj: Deployable,
object_guid: PlanetSideGUID,
pos: Vector3,
deletionEffect: Int
) extends Response
final case class SendHackMessageHackCleared(target_guid: PlanetSideGUID, unk1: Long, unk2: Long) extends Response
final case class HackObject(target_guid: PlanetSideGUID, unk1: Long, unk2: Long) extends Response
final case class SendPacket(packet: PlanetSideGamePacket) extends Response
@ -70,6 +69,11 @@ object LocalResponse {
) 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 StartRouterInternalTelepad(
router_guid: PlanetSideGUID,
obj_guid: PlanetSideGUID,
obj: Utility.InternalTelepad
) extends Response
final case class ToggleTeleportSystem(
router: Vehicle,
systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)]

View file

@ -1,87 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.services.local.support
import akka.actor.ActorRef
import net.psforever.objects.ce.Deployable
import net.psforever.objects.guid.{GUIDTask, TaskResolver}
import net.psforever.objects.zones.Zone
import net.psforever.objects.{BoomerDeployable, PlanetSideGameObject, TurretDeployable}
import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.services.RemoverActor
import scala.concurrent.duration._
class DeployableRemover(taskResolver: ActorRef) extends RemoverActor(taskResolver) {
final val FirstStandardDuration: FiniteDuration = 3 minutes
final val SecondStandardDuration: FiniteDuration = 2 seconds
def InclusionTest(entry: RemoverActor.Entry): Boolean = entry.obj.isInstanceOf[Deployable]
def InitialJob(entry: RemoverActor.Entry): Unit = {}
def FirstJob(entry: RemoverActor.Entry): Unit = {
val obj = entry.obj
obj match {
case boomer: BoomerDeployable =>
FirstJobBoomer(boomer, entry)
case _ => ;
}
entry.zone.Deployables ! Zone.Deployable.Dismiss(obj.asInstanceOf[PlanetSideGameObject with Deployable])
}
def FirstJobBoomer(obj: BoomerDeployable, entry: RemoverActor.Entry): Unit = {
obj.Trigger match {
case Some(trigger) =>
if (trigger.HasGUID) {
val guid = trigger.GUID
Zone.EquipmentIs.Where(trigger, guid, entry.zone) match {
case Some(Zone.EquipmentIs.InContainer(container, index)) =>
container.Slot(index).Equipment = None
case Some(Zone.EquipmentIs.OnGround()) =>
entry.zone.Ground ! Zone.Ground.RemoveItem(guid)
case _ => ;
}
context.parent ! DeployableRemover.DeleteTrigger(guid, entry.zone)
}
case None => ;
}
}
override def SecondJob(entry: RemoverActor.Entry): Unit = {
val obj = entry.obj.asInstanceOf[PlanetSideGameObject with Deployable]
trace(s"Deleting a ${obj.Definition.Name} deployable")
context.parent ! DeployableRemover.EliminateDeployable(obj, obj.GUID, obj.Position, entry.zone)
super.SecondJob(entry)
}
def ClearanceTest(entry: RemoverActor.Entry): Boolean = !entry.zone.DeployableList.contains(entry.obj)
def DeletionTask(entry: RemoverActor.Entry): TaskResolver.GiveTask = {
entry.obj match {
case turret: TurretDeployable =>
GUIDTask.UnregisterDeployableTurret(turret)(entry.zone.GUID)
case boomer: BoomerDeployable =>
boomer.Trigger match {
case Some(trigger) =>
boomer.Trigger = None
boomer.Zone.tasks ! GUIDTask.UnregisterObjectTask(trigger)(entry.zone.GUID)
case None => ;
}
GUIDTask.UnregisterObjectTask(boomer)(entry.zone.GUID)
case obj =>
GUIDTask.UnregisterObjectTask(obj)(entry.zone.GUID)
}
}
}
object DeployableRemover {
final case class EliminateDeployable(
obj: PlanetSideGameObject with Deployable,
guid: PlanetSideGUID,
position: Vector3,
zone: Zone
)
final case class DeleteTrigger(trigger_guid: PlanetSideGUID, zone: Zone)
}

View file

@ -1,143 +1,13 @@
// Copyright (c) 2017 PSForever
package net.psforever.services.local.support
import akka.actor.Cancellable
import net.psforever.objects.zones.Zone
import net.psforever.objects._
import net.psforever.services.support.{SimilarityComparator, SupportActor}
import scala.concurrent.duration._
class RouterTelepadActivation extends SupportActor[RouterTelepadActivation.Entry] {
var activationTask: Cancellable = Default.Cancellable
var telepadList: List[RouterTelepadActivation.Entry] = List()
val sameEntryComparator = new SimilarityComparator[RouterTelepadActivation.Entry]() {
def Test(entry1: RouterTelepadActivation.Entry, entry2: RouterTelepadActivation.Entry): Boolean = {
(entry1.obj eq entry2.obj) && (entry1.zone eq entry2.zone) && entry1.obj.GUID == entry2.obj.GUID
}
}
val firstStandardTime: FiniteDuration = 60 seconds
def InclusionTest(entry: RouterTelepadActivation.Entry): Boolean = {
val obj = entry.obj
obj.isInstanceOf[TelepadDeployable] && !obj.asInstanceOf[TelepadDeployable].Active
}
def receive: Receive =
entryManagementBehaviors
.orElse {
case RouterTelepadActivation.AddTask(obj, zone, duration) =>
val entry = RouterTelepadActivation.Entry(obj, zone, duration.getOrElse(firstStandardTime).toNanos)
if (InclusionTest(entry) && !telepadList.exists(test => sameEntryComparator.Test(test, entry))) {
if (entry.duration == 0) {
//skip the queue altogether
ActivationTask(entry)
} else if (telepadList.isEmpty) {
//we were the only entry so the event must be started from scratch
telepadList = List(entry)
trace(s"an activation task has been added: $entry")
RetimeFirstTask()
} else {
//unknown number of entries; append, sort, then re-time tasking
val oldHead = telepadList.head
if (!telepadList.exists(test => sameEntryComparator.Test(test, entry))) {
telepadList = (telepadList :+ entry).sortBy(entry => entry.time + entry.duration)
trace(s"an activation task has been added: $entry")
if (oldHead != telepadList.head) {
RetimeFirstTask()
}
} else {
trace(s"$obj is already queued")
}
}
} else {
trace(s"$obj either does not qualify for this behavior or is already queued")
}
//private messages from self to self
case RouterTelepadActivation.TryActivate() =>
activationTask.cancel()
val now: Long = System.nanoTime
val (in, out) = telepadList.partition(entry => { now - entry.time >= entry.duration })
telepadList = out
in.foreach { ActivationTask }
RetimeFirstTask()
trace(s"router activation task has found ${in.size} items to process")
case _ => ;
}
/**
* Common function to reset the first task's delayed execution.
* Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool.
* @param now the time (in nanoseconds);
* defaults to the current time (in nanoseconds)
*/
def RetimeFirstTask(now: Long = System.nanoTime): Unit = {
activationTask.cancel()
if (telepadList.nonEmpty) {
val short_timeout: FiniteDuration =
math.max(1, telepadList.head.duration - (now - telepadList.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global
activationTask = context.system.scheduler.scheduleOnce(short_timeout, self, RouterTelepadActivation.TryActivate())
}
}
def HurrySpecific(targets: List[PlanetSideGameObject], zone: Zone): Unit = {
PartitionTargetsFromList(telepadList, targets.map { RouterTelepadActivation.Entry(_, zone, 0) }, zone) match {
case (Nil, _) =>
debug(s"no tasks matching the targets $targets have been hurried")
case (in, out) =>
debug(s"the following tasks have been hurried: $in")
telepadList = out
if (out.nonEmpty) {
RetimeFirstTask()
}
in.foreach { ActivationTask }
}
}
def HurryAll(): Unit = {
trace("all tasks have been hurried")
activationTask.cancel()
telepadList.foreach {
ActivationTask
}
telepadList = Nil
}
def ClearSpecific(targets: List[PlanetSideGameObject], zone: Zone): Unit = {
PartitionTargetsFromList(telepadList, targets.map { RouterTelepadActivation.Entry(_, zone, 0) }, zone) match {
case (Nil, _) =>
debug(s"no tasks matching the targets $targets have been cleared")
case (in, out) =>
debug(s"the following tasks have been cleared: $in")
telepadList = out //.sortBy(entry => entry.time + entry.duration)
if (out.nonEmpty) {
RetimeFirstTask()
}
}
}
def ClearAll(): Unit = {
trace("all tasks have been cleared")
activationTask.cancel()
telepadList = Nil
}
def ActivationTask(entry: SupportActor.Entry): Unit = {
entry.obj.asInstanceOf[TelepadDeployable].Active = true
context.parent ! RouterTelepadActivation.ActivateTeleportSystem(entry.obj, entry.zone)
}
}
object RouterTelepadActivation {
final case class Entry(_obj: PlanetSideGameObject, _zone: Zone, _duration: Long)
extends SupportActor.Entry(_obj, _zone, _duration)
final case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None)
final case class TryActivate()
final case class ActivateTeleportSystem(telepad: PlanetSideGameObject, zone: Zone)
}

View file

@ -785,8 +785,6 @@ object Zones {
60 seconds
case _: DeployableSource =>
60 seconds
case _: ComplexDeployableSource =>
60 seconds
case _ =>
0 seconds
}

View file

@ -0,0 +1,353 @@
// Copyright (c) 2021 PSForever
package objects
import akka.actor.{ActorRef, Props}
import akka.testkit.TestProbe
import base.{ActorTest, FreedContextActorTest}
import net.psforever.objects.avatar.{Avatar, Certification, PlayerControl}
import net.psforever.objects.{ConstructionItem, Deployables, GlobalDefinitions, Player}
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.guid.{NumberPoolHub, TaskResolver}
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.zones.{Zone, ZoneDeployableActor, ZoneMap}
import net.psforever.packet.game._
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types._
import scala.collection.mutable.ListBuffer
import scala.concurrent.duration._
class DeployableBehaviorSetupTest extends ActorTest {
val eventsProbe = new TestProbe(system)
val jmine = Deployables.Make(DeployedItem.jammer_mine)() //guid=1
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
}
guid.register(jmine, number = 1)
jmine.Faction = PlanetSideEmpire.TR
jmine.Position = Vector3(1,2,3)
jmine.Orientation = Vector3(4,5,6)
"DeployableBehavior" should {
"perform self-setup" in {
assert(deployableList.isEmpty, "self-setup test - deployable list is not empty")
zone.Deployables ! Zone.Deployable.Build(jmine)
val eventsMsgs = eventsProbe.receiveN(3, 10.seconds)
eventsMsgs.head match {
case LocalServiceMessage(
"test",
LocalAction.TriggerEffectLocation(PlanetSideGUID(0), "spawn_object_effect", Vector3(1,2,3), Vector3(4,5,6))
) => ;
case _ => assert(false, "self-setup test - no spawn fx")
}
eventsMsgs(1) match {
case AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(0), obj)) =>
assert(obj eq jmine, "self-setup test - not same mine")
case _ =>
assert( false, "self-setup test - wrong deploy message")
}
eventsMsgs(2) match {
case LocalServiceMessage(
"TR",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Build,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.DisruptorMine, Vector3(1,2,3), PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "self-setup test - no icon or wrong icon")
}
assert(deployableList.contains(jmine), "self-setup test - deployable not appended to list")
}
}
}
class DeployableBehaviorSetupOwnedP1Test extends ActorTest {
val eventsProbe = new TestProbe(system)
val avatar = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)
val player = Player(avatar) //guid=3
val jmine = Deployables.Make(DeployedItem.jammer_mine)() //guid=1
val citem = new ConstructionItem(GlobalDefinitions.ace) //guid = 2
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Players = List(avatar)
override def LivePlayers = List(player)
}
guid.register(jmine, number = 1)
guid.register(citem, number = 2)
guid.register(player, number = 3)
jmine.Faction = PlanetSideEmpire.TR
jmine.Position = Vector3(1,2,3)
jmine.Orientation = Vector3(4,5,6)
jmine.AssignOwnership(player)
"DeployableBehavior" should {
"receive setup functions after asking owner" in {
val playerProbe = new TestProbe(system)
player.Actor = playerProbe.ref
assert(deployableList.isEmpty, "owned setup test, 1 - deployable list is not empty")
zone.Deployables ! Zone.Deployable.BuildByOwner(jmine, player, citem)
playerProbe.receiveOne(200.milliseconds) match {
case Player.BuildDeployable(a, b) =>
assert((a eq jmine) && (b eq citem), "owned setup test, 1 - process echoing wrong mine or wrong construction item")
case _ =>
assert(false, "owned setup test, 1 - not echoing messages to owner")
}
}
}
}
class DeployableBehaviorSetupOwnedP2Test extends FreedContextActorTest {
val eventsProbe = new TestProbe(system)
val avatar = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)
val player = Player(avatar) //guid=3
val jmine = Deployables.Make(DeployedItem.jammer_mine)() //guid=1
val citem = new ConstructionItem(GlobalDefinitions.ace) //guid = 2
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Players = List(avatar)
override def LivePlayers = List(player)
override def tasks: ActorRef = eventsProbe.ref
}
guid.register(jmine, number = 1)
guid.register(citem, number = 2)
guid.register(player, number = 3)
guid.register(avatar.locker, number = 4)
jmine.Faction = PlanetSideEmpire.TR
jmine.Position = Vector3(1,2,3)
jmine.Orientation = Vector3(4,5,6)
jmine.AssignOwnership(player)
avatar.deployables.UpdateMaxCounts(Set(Certification.CombatEngineering, Certification.AssaultEngineering))
player.Zone = zone
player.Slot(slot = 0).Equipment = citem
player.Actor = system.actorOf(Props(classOf[PlayerControl], player, null), name = "deployable-test-player-control")
"DeployableBehavior" should {
"perform setup functions after asking owner" in {
assert(player.Slot(slot = 0).Equipment.contains(citem), "owned setup test, 2 - player hand is empty")
assert(deployableList.isEmpty, "owned setup test, 2 - deployable list is not empty")
assert(!avatar.deployables.Contains(jmine), "owned setup test, 2 - avatar already owns deployable")
zone.Deployables ! Zone.Deployable.BuildByOwner(jmine, player, citem)
//assert(false, "test needs to be fixed")
val eventsMsgs = eventsProbe.receiveN(8, 10.seconds)
eventsMsgs.head match {
case AvatarServiceMessage(
"TestCharacter1",
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectDeployedMessage(0, "jammer_mine", DeployOutcome.Success, 1, 20)
)
) => ;
case _ =>
assert(false, "owned setup test, 2 - did not receive build confirmation")
}
eventsMsgs(1) match {
case LocalServiceMessage("TestCharacter1", LocalAction.DeployableUIFor(DeployedItem.jammer_mine)) => ;
case _ => assert(false, "owned setup test, 2 - did not receive ui update")
}
eventsMsgs(2) match {
case LocalServiceMessage(
"test",
LocalAction.TriggerEffectLocation(PlanetSideGUID(3), "spawn_object_effect", Vector3(1,2,3), Vector3(4,5,6))
) => ;
case _ => assert(false, "owned setup test, 2 - no spawn fx")
}
eventsMsgs(3) match {
case AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(0), obj)) =>
assert(obj eq jmine, "owned setup test, 2 - not same mine")
case _ =>
assert( false, "owned setup test, 2 - wrong deploy message")
}
//the message order can be jumbled from here-on
eventsMsgs(4) match {
case LocalServiceMessage(
"TR",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Build,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.DisruptorMine, Vector3(1,2,3), PlanetSideGUID(3))
)
) => ;
case _ =>
assert(false, "owned setup test, 2 - no icon or wrong icon")
}
eventsMsgs(5) match {
case AvatarServiceMessage(
"TestCharacter1",
AvatarAction.SendResponse(Service.defaultPlayerGUID, GenericObjectActionMessage(PlanetSideGUID(1), 21))
) => ;
case _ =>
assert(false, "owned setup test, 2 - build action not reset (GOAM21)")
}
eventsMsgs(6) match {
case AvatarServiceMessage("test", AvatarAction.ObjectDelete(Service.defaultPlayerGUID, PlanetSideGUID(2), 0)) => ;
case _ =>
assert(false, "owned setup test, 2 - construction tool not deleted")
}
eventsMsgs(7) match {
case TaskResolver.GiveTask(_, _) => ;
case _ =>
assert(false, "owned setup test, 2 - construction tool not unregistered")
}
assert(player.Slot(slot = 0).Equipment.isEmpty, "owned setup test, 2 - player hand should be empty")
assert(deployableList.contains(jmine), "owned setup test, 2 - deployable not appended to list")
assert(avatar.deployables.Contains(jmine), "owned setup test, 2 - avatar does not own deployable")
}
}
}
class DeployableBehaviorDeconstructTest extends ActorTest {
val eventsProbe = new TestProbe(system)
val jmine = Deployables.Make(DeployedItem.jammer_mine)() //guid = 1
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def tasks: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
}
guid.register(jmine, number = 1)
jmine.Faction = PlanetSideEmpire.TR
jmine.Position = Vector3(1,2,3)
jmine.Orientation = Vector3(4,5,6)
"DeployableBehavior" should {
"deconstruct, by self" in {
zone.Deployables ! Zone.Deployable.Build(jmine)
eventsProbe.receiveN(3, 10.seconds) //all of the messages from the construction (see other testing)
assert(deployableList.contains(jmine), "deconstruct test - deployable not appended to list")
jmine.Actor ! Deployable.Deconstruct()
val eventsMsgs = eventsProbe.receiveN(3, 10.seconds)
eventsMsgs.head match {
case LocalServiceMessage("test", LocalAction.EliminateDeployable(`jmine`, PlanetSideGUID(1), Vector3(1,2,3), 2)) => ;
case _ => assert(false, "deconstruct test - not eliminating deployable")
}
eventsMsgs(1) match {
case LocalServiceMessage(
"TR",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.DisruptorMine, Vector3(1, 2, 3), PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "owned deconstruct test - not removing icon")
}
eventsMsgs(2) match {
case TaskResolver.GiveTask(_, _) => ;
case _ => assert(false, "deconstruct test - not unregistering deployable")
}
assert(!deployableList.contains(jmine), "deconstruct test - deployable not removed from list")
}
}
}
class DeployableBehaviorDeconstructOwnedTest extends FreedContextActorTest {
val eventsProbe = new TestProbe(system)
val avatar = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)
val player = Player(avatar) //guid=3
val jmine = Deployables.Make(DeployedItem.jammer_mine)() //guid=1
val citem = new ConstructionItem(GlobalDefinitions.ace) //guid = 2
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Players = List(avatar)
override def LivePlayers = List(player)
override def tasks: ActorRef = eventsProbe.ref
}
guid.register(jmine, number = 1)
guid.register(citem, number = 2)
guid.register(player, number = 3)
guid.register(avatar.locker, number = 4)
jmine.Faction = PlanetSideEmpire.TR
jmine.Position = Vector3(1,2,3)
jmine.Orientation = Vector3(4,5,6)
jmine.AssignOwnership(player)
avatar.deployables.UpdateMaxCounts(Set(Certification.CombatEngineering, Certification.AssaultEngineering))
player.Zone = zone
player.Slot(slot = 0).Equipment = citem
player.Actor = system.actorOf(Props(classOf[PlayerControl], player, null), name = "deployable-test-player-control")
"DeployableBehavior" should {
"deconstruct and alert owner" in {
zone.Deployables ! Zone.Deployable.BuildByOwner(jmine, player, citem)
eventsProbe.receiveN(8, 10.seconds)
assert(deployableList.contains(jmine), "owned deconstruct test - deployable not appended to list")
assert(avatar.deployables.Contains(jmine), "owned deconstruct test - avatar does not own deployable")
jmine.Actor ! Deployable.Deconstruct()
val eventsMsgs = eventsProbe.receiveN(4, 10.seconds)
eventsMsgs.head match {
case LocalServiceMessage("test", LocalAction.EliminateDeployable(`jmine`, PlanetSideGUID(1), Vector3(1,2,3), 2)) => ;
case _ => assert(false, "owned deconstruct test - not eliminating deployable")
}
eventsMsgs(1) match {
case LocalServiceMessage("TestCharacter1", LocalAction.DeployableUIFor(DeployedItem.jammer_mine)) => ;
case _ => assert(false, "")
}
eventsMsgs(2) match {
case LocalServiceMessage(
"TR",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.DisruptorMine, Vector3(1, 2, 3), PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "owned deconstruct test - not removing icon")
}
eventsMsgs(3) match {
case TaskResolver.GiveTask(_, _) => ;
case _ => assert(false, "owned deconstruct test - not unregistering deployable")
}
assert(deployableList.isEmpty, "owned deconstruct test - deployable still in list")
assert(!avatar.deployables.Contains(jmine), "owned deconstruct test - avatar still owns deployable")
}
}
}
object DeployableBehaviorTest {
//...
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package objects
import akka.actor.{Actor, Props}
import akka.actor.{Actor, ActorRef, Props}
import akka.testkit.TestProbe
import base.ActorTest
import net.psforever.objects.ballistics._
@ -10,20 +10,20 @@ import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.mount.{MountInfo, Mountable}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.objects.zones.{Zone, ZoneDeployableActor, ZoneMap}
import net.psforever.objects.{TurretDeployable, _}
import net.psforever.packet.game.{DeployableIcon, DeployableInfo, DeploymentAction}
import net.psforever.types._
import org.specs2.mutable.Specification
import net.psforever.services.{RemoverActor, Service}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.support.SupportActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import scala.collection.mutable.ListBuffer
import scala.concurrent.duration._
class DeployableTest extends Specification {
@ -307,25 +307,30 @@ class ShieldGeneratorDeployableTest extends Specification {
class ExplosiveDeployableJammerTest extends ActorTest {
val guid = new NumberPoolHub(new MaxNumberSource(10))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
}
val activityProbe = TestProbe()
val avatarProbe = TestProbe()
val localProbe = TestProbe()
zone.Activity = activityProbe.ref
zone.AvatarEvents = avatarProbe.ref
zone.LocalEvents = localProbe.ref
val eventsProbe = new TestProbe(system)
val j_mine = Deployables.Make(DeployedItem.jammer_mine)().asInstanceOf[ExplosiveDeployable] //guid=1
val player1 =
Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3
player1.Spawn()
val player2 =
Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4
player2.Spawn()
val avatar1 = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)
val player1 = Player(avatar1) //guid=3
val avatar2 = Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)
val player2 = Player(avatar2) //guid=4
val weapon = Tool(GlobalDefinitions.jammer_grenade) //guid=5
val deployableList = new ListBuffer()
val zone = new Zone("test", new ZoneMap("test"), 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools() = {}
GUID(guid)
override def Activity: ActorRef = eventsProbe.ref
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Players = List(avatar1, avatar2)
override def LivePlayers = List(player1, player2)
override def tasks: ActorRef = eventsProbe.ref
}
player1.Spawn()
player2.Spawn()
guid.register(j_mine, 1)
guid.register(player1, 3)
guid.register(player2, 4)
@ -355,51 +360,22 @@ class ExplosiveDeployableJammerTest extends ActorTest {
assert(!j_mine.Destroyed)
j_mine.Actor ! Vitality.Damage(applyDamageToJ)
val msg_local = localProbe.receiveN(4, 200 milliseconds)
val msg_avatar = avatarProbe.receiveOne(200 milliseconds)
activityProbe.expectNoMessage(200 milliseconds)
assert(
msg_local.head match {
case LocalServiceMessage("TestCharacter2", LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) =>
target eq j_mine
case _ => false
}
)
assert(
msg_local(1) match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.DisruptorMine, _, PlanetSideGUID(0))
)
) =>
true
case _ => false
}
)
assert(
msg_local(2) match {
case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) =>
(j_mine eq target) && (_zone eq zone)
case _ => false
}
)
assert(
msg_local(3) match {
case LocalServiceMessage.Deployables(RemoverActor.AddTask(target, _zone, _)) =>
(target eq j_mine) && (_zone eq zone)
case _ => false
}
)
assert(
msg_avatar match {
case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(1), _, Service.defaultPlayerGUID, _)) =>
true
case _ => false
}
)
val eventMsgs = eventsProbe.receiveN(2, 200 milliseconds)
eventMsgs.head match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
ValidPlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(ValidPlanetSideGUID(1), DeployableIcon.DisruptorMine, Vector3.Zero, ValidPlanetSideGUID(0))
)
) => ;
case _ => assert(false, "")
}
eventMsgs(1) match {
case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(1), _, Service.defaultPlayerGUID, _)) => ;
case _ => assert(false, "")
}
assert(j_mine.Destroyed)
}
}
@ -407,25 +383,36 @@ class ExplosiveDeployableJammerTest extends ActorTest {
class ExplosiveDeployableJammerExplodeTest extends ActorTest {
val guid = new NumberPoolHub(new MaxNumberSource(10))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
}
val activityProbe = TestProbe()
val avatarProbe = TestProbe()
val localProbe = TestProbe()
zone.Activity = activityProbe.ref
zone.AvatarEvents = avatarProbe.ref
zone.LocalEvents = localProbe.ref
val eventsProbe = new TestProbe(system)
val player1Probe = new TestProbe(system)
val player2Probe = new TestProbe(system)
val h_mine = Deployables.Make(DeployedItem.he_mine)().asInstanceOf[ExplosiveDeployable] //guid=2
val player1 =
Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3
player1.Spawn()
val player2 =
Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4
player2.Spawn()
val avatar1 = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)
val player1 = Player(avatar1) //guid=3
val avatar2 = Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)
val player2 = Player(avatar2) //guid=4
val weapon = Tool(GlobalDefinitions.jammer_grenade) //guid=5
val deployableList = new ListBuffer()
val zone = new Zone("test", new ZoneMap("test"), 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools() = {}
GUID(guid)
override def Activity: ActorRef = eventsProbe.ref
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Players = List(avatar1, avatar2)
override def LivePlayers = List(player1, player2)
override def tasks: ActorRef = eventsProbe.ref
}
player1.Spawn()
player1.Actor = player1Probe.ref
avatar2.deployables.AddOverLimit(h_mine) //cram it down your throat
player2.Spawn()
player2.Position = Vector3(10,0,0)
player2.Actor = player2Probe.ref
guid.register(h_mine, 2)
guid.register(player1, 3)
guid.register(player2, 4)
@ -451,66 +438,50 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
"ExplosiveDeployable" should {
"handle being jammered appropriately (detonation)" in {
assert(avatar2.deployables.Contains(h_mine))
assert(!h_mine.Destroyed)
h_mine.Actor ! Vitality.Damage(applyDamageToH)
val msg_local = localProbe.receiveN(5, 200 milliseconds)
val msg_avatar = avatarProbe.receiveOne(200 milliseconds)
val msg_activity = activityProbe.receiveOne(200 milliseconds)
assert(
msg_local.head match {
case LocalServiceMessage("test", LocalAction.Detonate(PlanetSideGUID(2), target)) => target eq h_mine
case _ => false
}
)
assert(
msg_local(1) match {
case LocalServiceMessage("TestCharacter2", LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) =>
target eq h_mine
case _ => false
}
)
assert(
msg_local(2) match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(2), DeployableIcon.HEMine, _, PlanetSideGUID(0))
)
) =>
true
case _ => false
}
)
assert(
msg_local(3) match {
case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) =>
(h_mine eq target) && (_zone eq zone)
case _ => false
}
)
assert(
msg_local(4) match {
case LocalServiceMessage.Deployables(RemoverActor.AddTask(target, _zone, _)) =>
(target eq h_mine) && (_zone eq zone)
case _ => false
}
)
assert(
msg_avatar match {
case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, Service.defaultPlayerGUID, _)) =>
true
case _ => false
}
)
assert(
msg_activity match {
case Zone.HotSpot.Conflict(target, attacker, _) => (target.Definition eq h_mine.Definition) && (attacker eq pSource)
case _ => false
}
)
val eventMsgs = eventsProbe.receiveN(5, 200 milliseconds)
val p1Msgs = player1Probe.receiveN(1, 200 milliseconds)
player2Probe.expectNoMessage(200 milliseconds)
eventMsgs.head match {
case Zone.HotSpot.Conflict(target, attacker, _)
if (target.Definition eq h_mine.Definition) && (attacker eq pSource) => ;
case _ => assert(false, "")
}
eventMsgs(1) match {
case LocalServiceMessage("test", LocalAction.Detonate(PlanetSideGUID(2), target))
if target eq h_mine => ;
case _ => assert(false, "")
}
eventMsgs(2) match {
case LocalServiceMessage("TestCharacter2", LocalAction.DeployableUIFor(DeployedItem.he_mine)) => ;
case _ => assert(false, "")
}
eventMsgs(3) match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(2), DeployableIcon.HEMine, _, PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "")
}
eventMsgs(4) match {
case AvatarServiceMessage(
"test",
AvatarAction.Destroy(PlanetSideGUID(2), PlanetSideGUID(3), Service.defaultPlayerGUID, Vector3.Zero)
) => ;
case _ => assert(false, "")
}
p1Msgs.head match {
case Vitality.Damage(_) => ;
case _ => assert(false, "")
}
assert(!avatar2.deployables.Contains(h_mine))
assert(h_mine.Destroyed)
}
}
@ -518,25 +489,36 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
class ExplosiveDeployableDestructionTest extends ActorTest {
val guid = new NumberPoolHub(new MaxNumberSource(10))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
}
val activityProbe = TestProbe()
val avatarProbe = TestProbe()
val localProbe = TestProbe()
zone.Activity = activityProbe.ref
zone.AvatarEvents = avatarProbe.ref
zone.LocalEvents = localProbe.ref
val eventsProbe = new TestProbe(system)
val player1Probe = new TestProbe(system)
val player2Probe = new TestProbe(system)
val h_mine = Deployables.Make(DeployedItem.he_mine)().asInstanceOf[ExplosiveDeployable] //guid=2
val player1 =
Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3
player1.Spawn()
val player2 =
Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4
player2.Spawn()
val avatar1 = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)
val player1 = Player(avatar1) //guid=3
val avatar2 = Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)
val player2 = Player(avatar2) //guid=4
val weapon = Tool(GlobalDefinitions.suppressor) //guid=5
val deployableList = new ListBuffer()
val zone = new Zone("test", new ZoneMap("test"), 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools() = {}
GUID(guid)
override def Activity: ActorRef = eventsProbe.ref
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Players = List(avatar1, avatar2)
override def LivePlayers = List(player1, player2)
override def tasks: ActorRef = eventsProbe.ref
}
player1.Spawn()
player1.Actor = player1Probe.ref
avatar2.deployables.AddOverLimit(h_mine) //cram it down your throat
player2.Spawn()
player2.Position = Vector3(10,0,0)
player2.Actor = player2Probe.ref
guid.register(h_mine, 2)
guid.register(player1, 3)
guid.register(player2, 4)
@ -561,6 +543,10 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
)
val applyDamageTo = resolved.calculate()
val activityProbe = TestProbe()
val avatarProbe = TestProbe()
val localProbe = TestProbe()
"ExplosiveDeployable" should {
"handle being destroyed" in {
h_mine.Health = h_mine.Definition.DamageDestroysAt + 1
@ -568,58 +554,35 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
assert(!h_mine.Destroyed)
h_mine.Actor ! Vitality.Damage(applyDamageTo)
val msg_local = localProbe.receiveN(5, 200 milliseconds)
val msg_avatar = avatarProbe.receiveOne(200 milliseconds)
activityProbe.expectNoMessage(200 milliseconds)
assert(
msg_local.head match {
case LocalServiceMessage("TestCharacter2", LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) =>
target eq h_mine
case _ => false
}
)
assert(
msg_local(1) match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(2), DeployableIcon.HEMine, _, PlanetSideGUID(0))
)
) =>
true
case _ => false
}
)
assert(
msg_local(2) match {
case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) =>
(h_mine eq target) && (_zone eq zone)
case _ => false
}
)
assert(
msg_local(3) match {
case LocalServiceMessage.Deployables(RemoverActor.AddTask(target, _zone, _)) =>
(target eq h_mine) && (_zone eq zone)
case _ => false
}
)
assert(
msg_local(4) match {
case LocalServiceMessage("test", LocalAction.TriggerEffect(_, "detonate_damaged_mine", PlanetSideGUID(2))) =>
true
case _ => false
}
)
assert(
msg_avatar match {
case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, Service.defaultPlayerGUID, _)) =>
true
case _ => false
}
)
val eventMsgs = eventsProbe.receiveN(4, 200 milliseconds)
player1Probe.expectNoMessage(200 milliseconds)
player2Probe.expectNoMessage(200 milliseconds)
eventMsgs.head match {
case LocalServiceMessage("TestCharacter2", LocalAction.DeployableUIFor(DeployedItem.he_mine)) => ;
case _ => assert(false, "")
}
eventMsgs(1) match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(2), DeployableIcon.HEMine, _, PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "")
}
eventMsgs(2) match {
case AvatarServiceMessage(
"test",
AvatarAction.Destroy(PlanetSideGUID(2), PlanetSideGUID(3), Service.defaultPlayerGUID, Vector3.Zero)
) => ;
case _ => assert(false, "")
}
eventMsgs(3) match {
case LocalServiceMessage("test", LocalAction.TriggerEffect(_, "detonate_damaged_mine", PlanetSideGUID(2))) => ;
case _ => assert(false, "")
}
assert(h_mine.Health <= h_mine.Definition.DamageDestroysAt)
assert(h_mine.Destroyed)
}

View file

@ -383,7 +383,7 @@ class EquipmentTest extends Specification {
obj.AmmoType mustEqual DeployedItem.he_mine
}
"when switching fire modes, ammo mode resets to the first entry" in {
"when switching fire modes, ammo mode should be maintained" in {
val obj: ConstructionItem = ConstructionItem(GlobalDefinitions.ace)
obj.NextFireMode
obj.AmmoType mustEqual DeployedItem.he_mine
@ -392,8 +392,8 @@ class EquipmentTest extends Specification {
obj.NextFireMode //spitfire_turret
obj.NextFireMode //motionalarmsensor
obj.NextFireMode //boomer
obj.NextFireMode
obj.AmmoType mustEqual DeployedItem.he_mine
obj.NextFireMode //?
obj.AmmoType mustEqual DeployedItem.jammer_mine
}
"qualify certifications that must be met before ammo types may be used" in {

View file

@ -0,0 +1,333 @@
// Copyright (c) 2021 PSForever
package objects
import akka.actor.{ActorRef, Props}
import akka.testkit.TestProbe
import base.ActorTest
import net.psforever.objects._
import net.psforever.objects.ce.{DeployableCategory, DeployedItem, TelepadLike}
import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.vehicles.{Utility, UtilityType, VehicleControl}
import net.psforever.objects.zones.{Zone, ZoneDeployableActor, ZoneMap}
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.packet.game._
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{DriveState, PlanetSideGUID, Vector3}
import scala.collection.mutable.ListBuffer
import scala.concurrent.duration._
class TelepadDeployableNoRouterTest extends ActorTest {
val eventsProbe = new TestProbe(system)
val telepad = Deployables.Make(DeployedItem.router_telepad_deployable)() //guid=1
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
}
guid.register(telepad, number = 1)
"TelepadDeployable" should {
"fail to activate without a router" in {
assert(deployableList.isEmpty, "no-router telepad deployable test - deployable list is not empty")
zone.Deployables ! Zone.Deployable.Build(telepad)
val eventsMsgs = eventsProbe.receiveN(4, 10.seconds)
eventsMsgs.head match {
case AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(0), obj)) =>
assert(obj eq telepad, "no-router telepad deployable testt - not same telepad")
case _ =>
assert( false, "no-router telepad deployable test - wrong deploy message")
}
eventsMsgs(1) match {
case LocalServiceMessage(
"NEUTRAL",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Build,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.RouterTelepad, Vector3.Zero, PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "no-router telepad deployable test - no icon or wrong icon")
}
eventsMsgs(2) match {
case LocalServiceMessage("test", LocalAction.EliminateDeployable(`telepad`, PlanetSideGUID(1), Vector3.Zero, 2)) => ;
case _ => assert(false, "no-router telepad deployable test - not eliminating deployable")
}
eventsMsgs(3) match {
case LocalServiceMessage(
"NEUTRAL",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.RouterTelepad, Vector3.Zero, PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "no-router telepad deployable test - no icon or wrong icon cleared")
}
assert(deployableList.isEmpty, "no-router telepad deployable test - deployable is being tracked")
}
}
}
class TelepadDeployableNoActivationTest extends ActorTest {
val eventsProbe = new TestProbe(system)
val routerProbe = new TestProbe(system)
val telepad = Deployables.Make(DeployedItem.router_telepad_deployable)() //guid=1
val router = Vehicle(GlobalDefinitions.router) //guid=2
val internal = router.Utility(UtilityType.internal_router_telepad_deployable).get //guid=3
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Vehicles: List[Vehicle] = List(router)
}
guid.register(telepad, number = 1)
guid.register(router, number = 2)
guid.register(internal, number = 3)
router.Actor = eventsProbe.ref
internal.Actor = routerProbe.ref
"TelepadDeployable" should {
"fail to activate without a connected router" in {
assert(deployableList.isEmpty, "no-activate telepad deployable test - deployable list is not empty")
zone.Deployables ! Zone.Deployable.Build(telepad)
val eventsMsgs = eventsProbe.receiveN(4, 10.seconds)
eventsMsgs.head match {
case AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(0), obj)) =>
assert(obj eq telepad, "no-activate telepad deployable testt - not same telepad")
case _ =>
assert( false, "no-activate telepad deployable test - wrong deploy message")
}
eventsMsgs(1) match {
case LocalServiceMessage(
"NEUTRAL",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Build,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.RouterTelepad, Vector3.Zero, PlanetSideGUID(0))
)
) => ;
case _ =>
assert( false, "no-activate telepad deployable test - no icon or wrong icon")
}
eventsMsgs(2) match {
case LocalServiceMessage("test", LocalAction.EliminateDeployable(`telepad`, PlanetSideGUID(1), Vector3.Zero, 2)) => ;
case _ => assert(false, "no-activate telepad deployable test - not eliminating deployable")
}
eventsMsgs(3) match {
case LocalServiceMessage(
"NEUTRAL",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.RouterTelepad, Vector3.Zero, PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "no-activate telepad deployable test - no icon or wrong icon")
}
routerProbe.expectNoMessage(100.millisecond)
assert(deployableList.isEmpty, "no-activate telepad deployable test - deployable is being tracked")
}
}
}
class TelepadDeployableAttemptTest extends ActorTest {
val eventsProbe = new TestProbe(system)
val routerProbe = new TestProbe(system)
val telepad = new TelepadDeployable(TelepadRouterTest.router_telepad_deployable) //guid=1
val router = Vehicle(GlobalDefinitions.router) //guid=2
val internal = router.Utility(UtilityType.internal_router_telepad_deployable).get //guid=3
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Vehicles: List[Vehicle] = List(router)
}
guid.register(telepad, number = 1)
guid.register(router, number = 2)
guid.register(internal, number = 3)
router.Actor = eventsProbe.ref
internal.Actor = routerProbe.ref
telepad.Router = PlanetSideGUID(2) //artificial
"TelepadDeployable" should {
"attempt to link with a connected router" in {
assert(deployableList.isEmpty, "link attempt telepad deployable test - deployable list is not empty")
zone.Deployables ! Zone.Deployable.Build(telepad)
val eventsMsgs = eventsProbe.receiveN(2, 10.seconds)
val routerMsgs = routerProbe.receiveN(1, 10.seconds)
eventsMsgs.head match {
case AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(0), obj)) =>
assert(obj eq telepad, "link attempt telepad deployable testt - not same telepad")
case _ =>
assert( false, "link attempt telepad deployable test - wrong deploy message")
}
eventsMsgs(1) match {
case LocalServiceMessage(
"NEUTRAL",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Build,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.RouterTelepad, Vector3.Zero, PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "link attempt telepad deployable test - no icon or wrong icon")
}
routerMsgs.head match {
case TelepadLike.RequestLink(tpad) if tpad eq telepad => ;
case _ => assert(false, "link attempt telepad deployable test - did not try to link")
}
assert(deployableList.contains(telepad), "link attempt telepad deployable test - deployable list is not empty")
}
}
}
class TelepadDeployableResponseFromRouterTest extends ActorTest {
val eventsProbe = new TestProbe(system)
val telepad = new TelepadDeployable(TelepadRouterTest.router_telepad_deployable) //guid=1
val router = Vehicle(GlobalDefinitions.router) //guid=2
val internal = router
.Utility(UtilityType.internal_router_telepad_deployable)
.get
.asInstanceOf[Utility.InternalTelepad] //guid=3
val deployableList = new ListBuffer()
val guid = new NumberPoolHub(new MaxNumberSource(max = 5))
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
private val deployables = system.actorOf(Props(classOf[ZoneDeployableActor], this, deployableList), name = "test-zone-deployables")
override def SetupNumberPools(): Unit = {}
GUID(guid)
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def VehicleEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Vehicles: List[Vehicle] = List(router)
}
guid.register(telepad, number = 1)
guid.register(router, number = 2)
guid.register(internal, number = 3)
guid.register(router.Utility(UtilityType.teleportpad_terminal).get, number = 4) //necessary
router.Zone = zone
router.Actor = system.actorOf(Props(classOf[VehicleControl], router), "test-router")
telepad.Router = PlanetSideGUID(2) //artificial
"TelepadDeployable" should {
"link with a connected router" in {
assert(!telepad.Active, "link to router test - telepad active earlier than intended (1)")
assert(!internal.Active, "link to router test - router internals active earlier than intended")
router.Actor.tell(Deployment.TryDeploy(DriveState.Deploying), new TestProbe(system).ref)
eventsProbe.receiveN(10, 10.seconds) //flush all messages related to deployment
assert(!telepad.Active, "link to router test - telepad active earlier than intended (2)")
assert(internal.Active, "link to router test - router internals active not active when expected")
assert(deployableList.isEmpty, "link to router test - deployable list is not empty")
zone.Deployables ! Zone.Deployable.Build(telepad)
val eventsMsgs = eventsProbe.receiveN(9, 10.seconds)
eventsMsgs.head match {
case AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(0), obj)) =>
assert(obj eq telepad, "link to router test - not same telepad")
case _ =>
assert( false, "link to router test - wrong deploy message")
}
eventsMsgs(1) match {
case LocalServiceMessage(
"NEUTRAL",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Build,
DeployableInfo(PlanetSideGUID(1), DeployableIcon.RouterTelepad, Vector3.Zero, PlanetSideGUID(0))
)
) => ;
case _ => assert(false, "link to router test - no icon or wrong icon")
}
eventsMsgs(2) match {
case LocalServiceMessage(
"test",
LocalAction.SendResponse(
ObjectCreateMessage(_, 744, PlanetSideGUID(3), Some(ObjectCreateMessageParent(PlanetSideGUID(2), 2)), _)
)
) => ;
case _ => assert(false, "link to router test - did not create the internal router telepad (1)")
}
eventsMsgs(3) match {
case LocalServiceMessage(
"test",
LocalAction.SendResponse(GenericObjectActionMessage(PlanetSideGUID(3), 27))
) => ;
case _ => assert(false, "link to router test - did not create the internal router telepad (2)")
}
eventsMsgs(4) match {
case LocalServiceMessage(
"test",
LocalAction.SendResponse(GenericObjectActionMessage(PlanetSideGUID(3), 30))
) => ;
case _ => assert(false, "link to router test - did not create the internal router telepad (3)")
}
eventsMsgs(5) match {
case LocalServiceMessage(
"test",
LocalAction.SendResponse(GenericObjectActionMessage(PlanetSideGUID(3), 27))
) => ;
case _ => assert(false, "link to router test - did not link the internal telepad (1)")
}
eventsMsgs(6) match {
case LocalServiceMessage(
"test",
LocalAction.SendResponse(GenericObjectActionMessage(PlanetSideGUID(3), 28))
) => ;
case _ => assert(false, "link to router test - did not link the internal telepad (2)")
}
eventsMsgs(7) match {
case LocalServiceMessage(
"test",
LocalAction.SendResponse(GenericObjectActionMessage(PlanetSideGUID(1), 27))
) => ;
case _ => assert(false, "link to router test - did not link the telepad (1)")
}
eventsMsgs(8) match {
case LocalServiceMessage(
"test",
LocalAction.SendResponse(GenericObjectActionMessage(PlanetSideGUID(1), 28))
) => ;
case _ => assert(false, "link to router test - did not link the telepad (2)")
}
assert(telepad.Active, "link to router test - telepad not active when expected")
assert(internal.Active, "link to router test - router internals active not active when expected (2)")
assert(deployableList.contains(telepad), "link to router test - deployable list is not empty")
}
}
}
object TelepadRouterTest {
val router_telepad_deployable = new TelepadDeployableDefinition(DeployedItem.router_telepad_deployable.id) {
Name = "test_telepad_dep"
DeployTime = Duration.create(1, "ms")
DeployCategory = DeployableCategory.Telepads
linkTime = 1.second
}
}

View file

@ -78,20 +78,6 @@ class LocalService5Test extends ActorTest {
}
}
class AlertDestroyDeployableTest extends ActorTest {
ServiceManager.boot(system)
val obj = new SensorDeployable(GlobalDefinitions.motionalarmsensor)
"LocalService" should {
"pass AlertDestroyDeployable" in {
val service = system.actorOf(Props(classOf[LocalService], Zone.Nowhere), "l_service")
service ! Service.Join("test")
service ! LocalServiceMessage("test", LocalAction.AlertDestroyDeployable(PlanetSideGUID(10), obj))
expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(0), LocalResponse.AlertDestroyDeployable(obj)))
}
}
}
class DeployableMapIconTest extends ActorTest {
ServiceManager.boot(system)

View file

@ -1,175 +0,0 @@
// Copyright (c) 2017 PSForever
package service
import akka.actor.Props
import base.ActorTest
import net.psforever.objects._
import net.psforever.objects.zones.Zone
import net.psforever.types.PlanetSideGUID
import net.psforever.services.local.support.RouterTelepadActivation
import net.psforever.services.support.SupportActor
import scala.concurrent.duration._
class RouterTelepadActivationTest extends ActorTest {
"RouterTelepadActivation" should {
"construct" in {
system.actorOf(Props[RouterTelepadActivation](), "activation-test-actor")
}
}
}
class RouterTelepadActivationSimpleTest extends ActorTest {
"RouterTelepadActivation" should {
"handle a task" in {
val telepad = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad.GUID = PlanetSideGUID(1)
val obj = system.actorOf(
Props(classOf[ActorTest.SupportActorInterface], Props[RouterTelepadActivation](), self),
"activation-test-actor"
)
obj ! RouterTelepadActivation.AddTask(telepad, Zone.Nowhere, Some(2 seconds))
expectMsg(3 seconds, RouterTelepadActivation.ActivateTeleportSystem(telepad, Zone.Nowhere))
}
}
}
class RouterTelepadActivationComplexTest extends ActorTest {
"RouterTelepadActivation" should {
"handle multiple tasks" in {
val telepad1 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad1.GUID = PlanetSideGUID(1)
val telepad2 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad2.GUID = PlanetSideGUID(2)
val telepad3 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad3.GUID = PlanetSideGUID(3)
val obj = system.actorOf(
Props(classOf[ActorTest.SupportActorInterface], Props[RouterTelepadActivation](), self),
"activation-test-actor"
)
obj ! RouterTelepadActivation.AddTask(telepad1, Zone.Nowhere, Some(2 seconds))
obj ! RouterTelepadActivation.AddTask(telepad2, Zone.Nowhere, Some(3 seconds))
obj ! RouterTelepadActivation.AddTask(telepad3, Zone.Nowhere, Some(1 seconds))
val msgs = receiveN(3, 5 seconds) //organized by duration
assert(msgs.head.isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs.head.asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad3)
assert(msgs(1).isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs(1).asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad1)
assert(msgs(2).isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs(2).asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad2)
}
}
}
class RouterTelepadActivationHurryTest extends ActorTest {
"RouterTelepadActivation" should {
"hurry specific tasks" in {
val telepad1 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad1.GUID = PlanetSideGUID(1)
val telepad2 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad2.GUID = PlanetSideGUID(2)
val telepad3 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad3.GUID = PlanetSideGUID(3)
val obj = system.actorOf(
Props(classOf[ActorTest.SupportActorInterface], Props[RouterTelepadActivation](), self),
"activation-test-actor"
)
obj ! RouterTelepadActivation.AddTask(telepad1, Zone.Nowhere, Some(2 seconds))
obj ! RouterTelepadActivation.AddTask(telepad2, Zone.Nowhere, Some(2 seconds))
obj ! RouterTelepadActivation.AddTask(telepad3, Zone.Nowhere, Some(2 seconds))
obj ! SupportActor.HurrySpecific(List(telepad1, telepad2), Zone.Nowhere)
val msgs = receiveN(2, 1 seconds)
assert(msgs.head.isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs.head.asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad1)
assert(msgs(1).isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs(1).asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad2)
val last = receiveOne(3 seconds)
assert(last.isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(last.asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad3)
}
}
}
class RouterTelepadActivationHurryAllTest extends ActorTest {
"RouterTelepadActivation" should {
"hurry all tasks" in {
val telepad1 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad1.GUID = PlanetSideGUID(1)
val telepad2 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad2.GUID = PlanetSideGUID(2)
val telepad3 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad3.GUID = PlanetSideGUID(3)
val obj = system.actorOf(
Props(classOf[ActorTest.SupportActorInterface], Props[RouterTelepadActivation](), self),
"activation-test-actor"
)
obj ! RouterTelepadActivation.AddTask(telepad1, Zone.Nowhere, Some(7 seconds))
obj ! RouterTelepadActivation.AddTask(telepad2, Zone.Nowhere, Some(5 seconds))
obj ! RouterTelepadActivation.AddTask(telepad3, Zone.Nowhere, Some(6 seconds))
obj ! SupportActor.HurryAll()
val msgs =
receiveN(
3,
4 seconds
) //organized by duration; note: all messages received before the earliest task should be performed
assert(msgs.head.isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs.head.asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad2)
assert(msgs(1).isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs(1).asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad3)
assert(msgs(2).isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs(2).asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad1)
}
}
}
class RouterTelepadActivationClearTest extends ActorTest {
"RouterTelepadActivation" should {
"clear specific tasks" in {
val telepad1 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad1.GUID = PlanetSideGUID(1)
val telepad2 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad2.GUID = PlanetSideGUID(2)
val telepad3 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad3.GUID = PlanetSideGUID(3)
val obj = system.actorOf(
Props(classOf[ActorTest.SupportActorInterface], Props[RouterTelepadActivation](), self),
"activation-test-actor"
)
obj ! RouterTelepadActivation.AddTask(telepad1, Zone.Nowhere, Some(2 seconds))
obj ! RouterTelepadActivation.AddTask(telepad2, Zone.Nowhere, Some(2 seconds))
obj ! RouterTelepadActivation.AddTask(telepad3, Zone.Nowhere, Some(2 seconds))
obj ! SupportActor.ClearSpecific(List(telepad1, telepad2), Zone.Nowhere)
val msgs = receiveN(1, 3 seconds) //should only receive telepad3
assert(msgs.head.isInstanceOf[RouterTelepadActivation.ActivateTeleportSystem])
assert(msgs.head.asInstanceOf[RouterTelepadActivation.ActivateTeleportSystem].telepad == telepad3)
}
}
}
class RouterTelepadActivationClearAllTest extends ActorTest {
"RouterTelepadActivation" should {
"clear all tasks" in {
val telepad1 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad1.GUID = PlanetSideGUID(1)
val telepad2 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad2.GUID = PlanetSideGUID(2)
val telepad3 = new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
telepad3.GUID = PlanetSideGUID(3)
val obj = system.actorOf(
Props(classOf[ActorTest.SupportActorInterface], Props[RouterTelepadActivation](), self),
"activation-test-actor"
)
obj ! RouterTelepadActivation.AddTask(telepad1, Zone.Nowhere, Some(2 seconds))
obj ! RouterTelepadActivation.AddTask(telepad2, Zone.Nowhere, Some(2 seconds))
obj ! RouterTelepadActivation.AddTask(telepad3, Zone.Nowhere, Some(2 seconds))
obj ! SupportActor.ClearAll()
expectNoMessage(4 seconds)
}
}
}