From 2f9c4a7cf2fa5f7a62a446fffe9708a8576ce3a8 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Wed, 2 Jun 2021 11:51:38 -0400 Subject: [PATCH] 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? --- .../actors/session/SessionActor.scala | 823 +++--------------- .../net/psforever/actors/zone/ZoneActor.scala | 8 +- .../net/psforever/login/WorldSession.scala | 18 + .../psforever/objects/BoomerDeployable.scala | 87 +- .../psforever/objects/ConstructionItem.scala | 18 +- .../net/psforever/objects/Deployables.scala | 172 +++- .../objects/ExplosiveDeployable.scala | 50 +- .../psforever/objects/GlobalDefinitions.scala | 78 +- .../scala/net/psforever/objects/Player.scala | 11 +- .../scala/net/psforever/objects/Players.scala | 313 ++++++- .../psforever/objects/SensorDeployable.scala | 40 +- .../objects/ShieldGeneratorDeployable.scala | 33 +- .../net/psforever/objects/SpecialEmp.scala | 10 +- .../scala/net/psforever/objects/Telepad.scala | 4 +- .../psforever/objects/TelepadDeployable.scala | 138 ++- .../psforever/objects/TrapDeployable.scala | 30 +- .../psforever/objects/TurretDeployable.scala | 59 +- .../net/psforever/objects/Vehicles.scala | 32 +- .../objects/avatar/DeployableToolbox.scala | 41 +- .../objects/avatar/PlayerControl.scala | 366 +++++--- .../ballistics/ComplexDeployableSource.scala | 55 -- .../objects/ballistics/DeployableSource.scala | 6 +- .../objects/ballistics/SourceEntry.scala | 11 +- .../objects/ce/ComplexDeployable.scala | 22 - .../net/psforever/objects/ce/Deployable.scala | 47 +- .../objects/ce/DeployableBehavior.scala | 316 +++++++ .../objects/ce/SimpleDeployable.scala | 11 - .../psforever/objects/ce/TelepadLike.scala | 120 ++- ...ition.scala => DeployableDefinition.scala} | 40 +- .../converter/SmallDeployableConverter.scala | 7 +- .../converter/VehicleConverter.scala | 10 +- .../objects/equipment/FireModeSwitch.scala | 14 +- .../objects/vehicles/VehicleControl.scala | 19 +- .../psforever/objects/vital/Vitality.scala | 9 - .../resolution/ResolutionCalculations.scala | 6 +- .../net/psforever/objects/zones/Zone.scala | 57 +- .../objects/zones/ZoneDeployableActor.scala | 54 +- .../psforever/packet/control/Unknown30.scala | 5 +- .../DetailedConstructionToolData.scala | 16 +- .../services/avatar/AvatarService.scala | 2 +- .../avatar/AvatarServiceMessage.scala | 4 +- .../services/galaxy/GalaxyService.scala | 2 +- .../galaxy/GalaxyServiceMessage.scala | 2 +- .../galaxy/GalaxyServiceResponse.scala | 2 +- .../services/local/LocalService.scala | 253 ++---- .../services/local/LocalServiceMessage.scala | 29 +- .../services/local/LocalServiceResponse.scala | 24 +- .../local/support/DeployableRemover.scala | 87 -- .../support/RouterTelepadActivation.scala | 130 --- .../scala/net/psforever/zones/Zones.scala | 2 - .../objects/DeployableBehaviorTest.scala | 353 ++++++++ src/test/scala/objects/DeployableTest.scala | 375 ++++---- src/test/scala/objects/EquipmentTest.scala | 6 +- .../scala/objects/TelepadRouterTest.scala | 333 +++++++ src/test/scala/service/LocalServiceTest.scala | 14 - .../service/RouterTelepadActivationTest.scala | 175 ---- 56 files changed, 2825 insertions(+), 2124 deletions(-) delete mode 100644 src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala delete mode 100644 src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala create mode 100644 src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala delete mode 100644 src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala rename src/main/scala/net/psforever/objects/definition/{SimpleDeployableDefinition.scala => DeployableDefinition.scala} (61%) delete mode 100644 src/main/scala/net/psforever/services/local/support/DeployableRemover.scala create mode 100644 src/test/scala/objects/DeployableBehaviorTest.scala create mode 100644 src/test/scala/objects/TelepadRouterTest.scala delete mode 100644 src/test/scala/service/RouterTelepadActivationTest.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index fd6c0e2a..a94834e0 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -20,7 +20,6 @@ import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.locker.LockerContainer import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.containable.Containable -import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator @@ -57,10 +56,9 @@ import net.psforever.services.account.{AccountPersistenceService, PlayerToken, R import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} import net.psforever.services.chat.ChatService import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse} -import net.psforever.services.local.support.{CaptureFlagManager, HackCaptureActor, RouterTelepadActivation} +import net.psforever.services.local.support.{CaptureFlagManager, HackCaptureActor} import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} import net.psforever.services.properties.PropertyOverrideManager -import net.psforever.services.support.SupportActor import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} import net.psforever.services.hart.HartTimer import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} @@ -150,7 +148,7 @@ object SessionActor { private final case class NtuDischarging(tplayer: Player, vehicle: Vehicle, silo_guid: PlanetSideGUID) private final case class FinalizeDeployable( - obj: PlanetSideGameObject with Deployable, + obj: Deployable, tool: ConstructionItem, index: Int ) @@ -167,7 +165,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con MDC("connectionId") = connectionId private[this] val log = org.log4s.getLogger - private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) var avatarActor: typed.ActorRef[AvatarActor.Command] = context.spawnAnonymous(AvatarActor(context.self)) var chatActor: typed.ActorRef[ChatActor.Command] = context.spawnAnonymous(ChatActor(context.self, avatarActor)) var accountIntermediary: ActorRef = Default.Actor @@ -1038,140 +1035,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) => log.warn(s"${player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason") - case Zone.Deployable.DeployableIsBuilt(obj, tool) => - val index = player.Find(tool) match { - case Some(x) => - x - case None => - player.LastDrawnSlot - } - if (avatar.deployables.Accept(obj) || (avatar.deployables.Valid(obj) && !avatar.deployables.Contains(obj))) { - tool.Definition match { - case GlobalDefinitions.ace => - continent.LocalEvents ! LocalServiceMessage( - continent.id, - LocalAction.TriggerEffectLocation(player.GUID, "spawn_object_effect", obj.Position, obj.Orientation) - ) - case GlobalDefinitions.advanced_ace => - sendResponse( - GenericObjectActionMessage(player.GUID, 53) - ) //put fdu down; it will be removed from the client's holster - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PutDownFDU(player.GUID)) - case GlobalDefinitions.router_telepad => ; - case _ => - log.warn( - s"Zone.Deployable.DeployableIsBuilt: not sure what kind of construction item to animate - ${tool.Definition.Name}" - ) - } - import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce( - obj.Definition.DeployTime milliseconds, - self, - SessionActor.FinalizeDeployable(obj, tool, index) - ) - } else { - TryDropFDU(tool, index, obj.Position) - sendResponse(ObjectDeployedMessage.Failure(obj.Definition.Name)) - obj.Position = Vector3.Zero - obj.AssignOwnership(None) - continent.Deployables ! Zone.Deployable.Dismiss(obj) - } - - case SessionActor.FinalizeDeployable(obj: SensorDeployable, tool, index) => - //motion alarm sensor and sensor disruptor - DeployableBuildActivity(obj) - continent.LocalEvents ! LocalServiceMessage( - continent.id, - LocalAction.TriggerEffectInfo(player.GUID, "on", obj.GUID, true, 1000) - ) - CommonDestroyConstructionItem(tool, index) - FindReplacementConstructionItem(tool, index) - - case SessionActor.FinalizeDeployable(obj: BoomerDeployable, tool, index) => - //boomers - DeployableBuildActivity(obj) - //TODO sufficiently delete the tool - sendResponse(ObjectDeleteMessage(tool.GUID, 0)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(player.GUID, tool.GUID)) - continent.tasks ! GUIDTask.UnregisterEquipment(tool)(continent.GUID) - val trigger = new BoomerTrigger - trigger.Companion = obj.GUID - obj.Trigger = trigger - val holster = player.Slot(index) - if (holster.Equipment.contains(tool)) { - holster.Equipment = None - continent.tasks ! HoldNewEquipmentUp(player)(trigger, index) - } else { - //don't know where boomer trigger should go; drop it on the ground - continent.tasks ! NewItemDrop(player, continent)(trigger) - } - - case SessionActor.FinalizeDeployable(obj: ExplosiveDeployable, tool, index) => - //mines - DeployableBuildActivity(obj) - CommonDestroyConstructionItem(tool, index) - FindReplacementConstructionItem(tool, index) - - case SessionActor.FinalizeDeployable(obj: ComplexDeployable, tool, index) => - //tank_traps, spitfires, deployable field turrets and the deployable_shield_generator - DeployableBuildActivity(obj) - CommonDestroyConstructionItem(tool, index) - FindReplacementConstructionItem(tool, index) - - case SessionActor.FinalizeDeployable(obj: TelepadDeployable, tool, index) => - if (obj.Health > 0) { - val guid = obj.GUID - //router telepad deployable - val router = tool.asInstanceOf[Telepad].Router - //router must exist and be deployed - continent.GUID(router) match { - case Some(vehicle: Vehicle) => - val routerGUID = router.get - if (vehicle.Destroyed) { - //the Telepad was successfully deployed; but, before it could configure, its Router was destroyed - sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", "@Telepad_NoDeploy_RouterLost", None)) - continent.LocalEvents ! LocalServiceMessage.Deployables( - RemoverActor.AddTask(obj, continent, Some(0 seconds)) - ) - } else { - log.debug(s"FinalizeDeployable: setup for telepad #${guid.guid} in zone ${continent.id}") - obj.Router = routerGUID //necessary; forwards link to the router - DeployableBuildActivity(obj) - RemoveOldEquipmentFromInventory(player)(tool) - //it takes 60s for the telepad to become properly active - continent.LocalEvents ! LocalServiceMessage.Telepads(RouterTelepadActivation.AddTask(obj, continent)) - } - - case _ => - //the Telepad was successfully deployed; but, before it could configure, its Router was deconstructed - sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", "@Telepad_NoDeploy_RouterLost", None)) - continent.LocalEvents ! LocalServiceMessage.Deployables( - RemoverActor.AddTask(obj, continent, Some(0 seconds)) - ) - } - } - - case SessionActor.FinalizeDeployable(obj: PlanetSideGameObject with Deployable, tool, index) => - val guid = obj.GUID - val definition = obj.Definition - sendResponse(GenericObjectActionMessage(guid, 21)) //reset build cooldown - sendResponse(ObjectDeployedMessage.Failure(definition.Name)) - log.warn( - s"FinalizeDeployable: deployable ${definition.Item}@$guid not handled by specific case" - ) - log.warn( - s"FinalizeDeployable: deployable ${definition.Item}@$guid will be cleaned up, but may not get unregistered properly" - ) - TryDropFDU(tool, index, obj.Position) - obj.Position = Vector3.Zero - continent.Deployables ! Zone.Deployable.Dismiss(obj) - - //!!only dispatch Zone.Deployable.Dismiss from WorldSessionActor as cleanup if the target deployable was never fully introduced - case Zone.Deployable.DeployableIsDismissed(obj: TurretDeployable) => + //!!only dispatched to SessionActor as cleanup if the target deployable was never fully introduced + case Zone.Deployable.IsDismissed(obj: TurretDeployable) => continent.tasks ! GUIDTask.UnregisterDeployableTurret(obj)(continent.GUID) - //!!only dispatch Zone.Deployable.Dismiss from WorldSessionActor as cleanup if the target deployable was never fully introduced - case Zone.Deployable.DeployableIsDismissed(obj) => + //!!only dispatched to SessionActor as cleanup if the target deployable was never fully introduced + case Zone.Deployable.IsDismissed(obj) => continent.tasks ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) case ICS.ZonesResponse(zones) => @@ -1445,19 +1314,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - case Vitality.DamageResolution(target: TelepadDeployable, _) => - //telepads - if (target.Health <= 0) { - //update if destroyed - target.Destroyed = true - val guid = target.GUID - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(player.GUID, guid)) - Deployables.AnnounceDestroyDeployable(target, Some(0 seconds)) - } - - case Vitality.DamageResolution(target: PlanetSideGameObject, _) => - log.warn(s"DamageResolution: vital target ${target.Definition.Name} damage resolution not supported") - case ResponseToSelf(pkt) => sendResponse(pkt) @@ -1873,6 +1729,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con DropSpecialSlotItem() case AvatarResponse.Killed(mount) => + val cause = (player.LastDamage match { + case Some(reason) => (Some(reason), reason.adversarial) + case None => (None, None) + }) match { + case (_, Some(adversarial)) => adversarial.attacker.Name + case (Some(reason), None) => s"a ${reason.interaction.cause.getClass.getSimpleName}" + case _ => "an unfortunate circumstance" + } + log.info(s"${player.Name} has died, killed by $cause") val respawnTimer = 300.seconds //drop free hand item player.FreeHand.Equipment match { @@ -2303,27 +2168,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (player.HasGUID) player.GUID else PlanetSideGUID(0) reply match { - case LocalResponse.AlertDestroyDeployable(obj: BoomerDeployable) => - //the (former) owner (obj.OwnerName) should process this message - obj.Trigger match { - case Some(item: BoomerTrigger) => - FindEquipmentToDelete(item.GUID, item) - item.Companion = None - case _ => ; - } - avatar.deployables.Remove(obj) - UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(obj.Definition.Item)) - - case LocalResponse.AlertDestroyDeployable(obj) => - //the (former) owner (obj.OwnerName) should process this message - avatar.deployables.Remove(obj) - UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(obj.Definition.Item)) - case LocalResponse.DeployableMapIcon(behavior, deployInfo) => if (tplayer_guid != guid) { sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) } + case LocalResponse.DeployableUIFor(item) => + UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) + case LocalResponse.Detonate(dguid, obj: BoomerDeployable) => sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) sendResponse(PlanetsideAttributeMessage(dguid, 29, 1)) @@ -2345,9 +2197,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.DoorCloses(door_guid) => //door closes for everyone sendResponse(GenericObjectStateMsg(door_guid, 17)) - case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos) => + case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) => if (obj.Destroyed) { - DeconstructDeployable(obj, dguid, pos) + sendResponse(ObjectDeleteMessage(dguid, 0)) } else { obj.Destroyed = true DeconstructDeployable( @@ -2355,64 +2207,39 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con dguid, pos, obj.Orientation, - if (obj.MountPoints.isEmpty) 2 - else 1 + if (obj.MountPoints.isEmpty) 2 else 1 ) } - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos) => + case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) => if (obj.Destroyed || obj.Jammed || obj.Health == 0) { - DeconstructDeployable(obj, dguid, pos) + sendResponse(ObjectDeleteMessage(dguid, 0)) } else { obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, 2) + DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) } - case LocalResponse.EliminateDeployable(obj: ComplexDeployable, dguid, pos) => - if (obj.Destroyed) { - DeconstructDeployable(obj, dguid, pos) - } else { - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, 1) - } - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos) => + case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) => //if active, deactivate if (obj.Active) { obj.Active = false sendResponse(GenericObjectActionMessage(dguid, 29)) sendResponse(GenericObjectActionMessage(dguid, 30)) } - //determine if no replacement teleport system exists - continent.GUID(obj.Router) match { - case Some(router: Vehicle) => - //if the telepad was replaced, the new system is physically in place but not yet functional - if ( - router.Utility(UtilityType.internal_router_telepad_deployable) match { - case Some(internalTelepad: Utility.InternalTelepad) => - internalTelepad.Telepad.contains(dguid) //same telepad - case _ => true - } - ) { - //there is no replacement telepad; shut down the system - ToggleTeleportSystem(router, None) - } - case _ => ; - } //standard deployable elimination behavior if (obj.Destroyed) { - DeconstructDeployable(obj, dguid, pos) + sendResponse(ObjectDeleteMessage(dguid, 0)) } else { obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, 2) + DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType = 2) } - case LocalResponse.EliminateDeployable(obj, dguid, pos) => + case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) => if (obj.Destroyed) { - DeconstructDeployable(obj, dguid, pos) + sendResponse(ObjectDeleteMessage(dguid, 0)) } else { obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, 2) + DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) } case LocalResponse.SendHackMessageHackCleared(target_guid, unk1, unk2) => @@ -2479,12 +2306,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.RouterTelepadTransport(passenger_guid, src_guid, dest_guid) => UseRouterTelepadEffect(passenger_guid, src_guid, dest_guid) + case LocalResponse.SendResponse(msg) => + sendResponse(msg) + case LocalResponse.SetEmpire(object_guid, empire) => sendResponse(SetEmpireMessage(object_guid, empire)) - case LocalResponse.SendResponse(pkt) => - sendResponse(pkt) - case LocalResponse.ShuttleEvent(ev) => val msg = OrbitalShuttleTimeMsg( ev.u1, @@ -3550,10 +3377,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val guid = player.GUID val foundDeployables = continent.DeployableList.filter(obj => obj.OwnerName.contains(player.Name) && obj.Health > 0) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(foundDeployables, continent)) foundDeployables.foreach(obj => { - if (avatar.deployables.Add(obj)) { - obj.Owner = guid + if (avatar.deployables.AddOverLimit(obj)) { + obj.Actor ! Deployable.Ownership(player) } }) //render deployable objects @@ -4189,7 +4015,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ ChangeAmmoMessage(item_guid, unk1) => FindContainedEquipment match { case (Some(_), Some(obj: ConstructionItem)) => - PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex) + if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { + log.info( + s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" + ) + sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) + } case (Some(obj: PlanetSideServerObject), Some(tool: Tool)) => PerformToolAmmoChange(tool, obj) case (_, Some(obj)) => @@ -4202,23 +4033,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con FindEquipment match { case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => val originalModeIndex = obj.FireModeIndex - obj match { - case cItem: ConstructionItem => - NextConstructionItemFireMode(cItem, originalModeIndex) + if (obj match { + case citem: ConstructionItem => + val modeChanged = Deployables.performConstructionItemFireModeChange( + player.avatar.certifications, + citem, + originalModeIndex + ) + modeChanged case _ => - obj.NextFireMode - } - val modeIndex = obj.FireModeIndex - val tool_guid = obj.GUID - if (originalModeIndex == modeIndex) { - obj.FireModeIndex = originalModeIndex - sendResponse(ChangeFireModeMessage(tool_guid, originalModeIndex)) //reinforcement - } else { - log.info(s"${player.Name} is changing his ${obj.Definition.Name} to fire mode #$modeIndex") - sendResponse(ChangeFireModeMessage(tool_guid, modeIndex)) + obj.NextFireMode == originalModeIndex + }) { + val modeIndex = obj.FireModeIndex + obj match { + case citem: ConstructionItem => + log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)") + case _ => + log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex") + } + sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) continent.AvatarEvents ! AvatarServiceMessage( continent.id, - AvatarAction.ChangeFireMode(player.GUID, tool_guid, modeIndex) + AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex) ) } case Some(_) => @@ -4284,7 +4120,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con FindEquipment } else { FindEquipment match { - case Some(tool: Tool) => + case Some(tool: Tool) => //special cases //the decimator does not send a ChangeFireState_Start on the last shot if ( tool.Definition == GlobalDefinitions.phoenix && @@ -4309,9 +4145,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) Some(tool) case _ => - log.warn( - s"ChangeFireState_Stop: ${player.Name} never started firing item ${item_guid.guid} in the first place?" - ) + //log.warn(s"ChangeFireState_Stop: ${player.Name} never started firing item ${item_guid.guid} in the first place?") None } } @@ -4453,12 +4287,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) // Ignore non-equipment holsters //todo: check current suit holster slots? - if (held_holsters >= 0 && held_holsters < 5) { - player.Holsters()(held_holsters).Equipment match { + val isHolsters = held_holsters >= 0 && held_holsters < 5 + val equipment = player.Slot(held_holsters).Equipment.orElse { player.Slot(before).Equipment } + if (isHolsters) { + equipment match { case Some(unholsteredItem: Equipment) => log.info(s"${player.Name} has drawn a $unholsteredItem from its holster") if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) { - // Player has unholstered a REK - we need to set an atttribute on the REK itself to change the beam/icon colour to the correct one for the player's hack level + //rek beam/icon colour must match the player's correct hack level continent.AvatarEvents ! AvatarServiceMessage( player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, player.avatar.hackingSkillLevel()) @@ -4466,6 +4302,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case None => ; } + } else { + equipment match { + case Some(holsteredEquipment) => + log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down") + case None => + log.info(s"${player.Name} lowers ${player.Sex.possessive} hand") + } } // Stop using proximity terminals if player unholsters a weapon (which should re-trigger the proximity effect and re-holster the weapon) @@ -4539,24 +4382,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.warn(s"RequestDestroy: ${player.Name} must own vehicle in order to deconstruct it") } - case Some(obj: BoomerTrigger) => - if (FindEquipmentToDelete(object_guid, obj)) { - continent.GUID(obj.Companion) match { - case Some(boomer: BoomerDeployable) => - boomer.Trigger = None - continent.LocalEvents ! LocalServiceMessage.Deployables( - RemoverActor.AddTask(boomer, continent, Some(0 seconds)) - ) - //continent.Deployables ! Zone.Deployable.Dismiss(boomer) - case Some(thing) => - log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") - case None => ; - } - } - - case Some(obj: Equipment) => - FindEquipmentToDelete(object_guid, obj) - case Some(obj: Projectile) => if (obj.isResolved) { log.warn( @@ -4573,47 +4398,30 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - case Some(obj: BoomerDeployable) => - continent.LocalEvents ! LocalServiceMessage.Deployables( - RemoverActor.AddTask(obj, continent, Some(0 seconds)) - ) - obj.Trigger match { - case Some(trigger) => - obj.Trigger = None - val guid = trigger.GUID - Zone.EquipmentIs.Where(trigger, guid, continent) match { - case Some(Zone.EquipmentIs.InContainer(container, index)) => - container.Slot(index).Equipment = None - case Some(Zone.EquipmentIs.OnGround()) => - continent.Ground ! Zone.Ground.RemoveItem(guid) - case Some(Zone.EquipmentIs.Orphaned()) => - log.warn(s"RequestDestroy: boomer_trigger@$guid has been found but it seems to be orphaned") - case _ => ; - } - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ObjectDelete(PlanetSideGUID(0), guid) - ) - GUIDTask.UnregisterObjectTask(trigger)(continent.GUID) - - case None => ; + case Some(obj: BoomerTrigger) => + if (FindEquipmentToDelete(object_guid, obj)) { + continent.GUID(obj.Companion) match { + case Some(boomer: BoomerDeployable) => + boomer.Trigger = None + boomer.Actor ! Deployable.Deconstruct() + case Some(thing) => + log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") + case None => ; + } } - case Some(obj: TelepadDeployable) => - continent.LocalEvents ! LocalServiceMessage.Telepads(SupportActor.ClearSpecific(List(obj), continent)) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent)) - continent.LocalEvents ! LocalServiceMessage.Deployables( - RemoverActor.AddTask(obj, continent, Some(0 seconds)) - ) + case Some(obj: Deployable) => + if (session.account.gm || obj.Owner.isEmpty || obj.Owner.contains(player.GUID) || obj.Destroyed) { + obj.Actor ! Deployable.Deconstruct() + } else { + log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it") + } - case Some(obj: PlanetSideGameObject with Deployable) => - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent)) - continent.LocalEvents ! LocalServiceMessage.Deployables( - RemoverActor.AddTask(obj, continent, Some(0 seconds)) - ) + case Some(obj: Equipment) => + FindEquipmentToDelete(object_guid, obj) case Some(thing) => - log.warn(s"RequestDestroy: not allowed to delete object $thing") + log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}") case None => log.warn(s"RequestDestroy: object ${object_guid.guid} not found") @@ -5117,25 +4925,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => ; } - case msg @ DeployObjectMessage(guid, unk1, pos, orient, unk2) => - //the hand with the construction item is no longer drawn - //TODO consider player.Slot(player.LastDrawnSlot) + case msg @ DeployObjectMessage(guid, _, pos, orient, _) => (player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid) match { - case Some(slot) => - slot.Equipment - case None => - None + case Some(slot) => slot.Equipment + case None => None }) match { case Some(obj: ConstructionItem) => val ammoType = obj.AmmoType match { - case DeployedItem.portable_manned_turret => - GlobalDefinitions.PortableMannedTurret(player.Faction).Item //faction-specific turret - case turret => - turret + case DeployedItem.portable_manned_turret => GlobalDefinitions.PortableMannedTurret(player.Faction).Item + case dtype => dtype } log.info(s"${player.Name} is constructing a $ammoType deployable") CancelZoningProcessWithDescriptiveReason("cancel_use") - val dObj: PlanetSideGameObject with Deployable = Deployables.Make(ammoType)() + val dObj: Deployable = Deployables.Make(ammoType)() dObj.Position = pos dObj.Orientation = orient dObj.Faction = player.Faction @@ -5146,7 +4948,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => GUIDTask.RegisterObjectTask(dObj)(continent.GUID) } - continent.tasks ! CallBackForTask(tasking, continent.Deployables, Zone.Deployable.Build(dObj, obj)) + continent.tasks ! CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj)) case Some(obj) => log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") @@ -5765,9 +5567,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(player: Player) if attribute_type == 106 => avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attribute_value)) + case Some(obj) => + log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attribute_type to ${obj.Definition.Name}") + case _ => - log.warn(s"PlanetsideAttribute: echoing unknown attributes behavior $attribute_type back to ${player.Name}") - sendResponse(PlanetsideAttributeMessage(object_guid, attribute_type, attribute_value)) + log.warn(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attribute_type") } case msg @ FacilityBenefitShieldChargeRequestMessage(guid) => @@ -6200,24 +6004,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - 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) - ) - } - def AccessContainer(container: Container): Unit = { container match { case v: Vehicle => @@ -6772,10 +6558,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** * For a given facility structure, configure a client by dispatching the appropriate packets. - * Pay special attention to the details of `BuildingInfoUpdateMessage` when preparing this packet.
- *
- * 24 Janurtay 2019:
- * Manual `BIUM` construction to alleviate player login. * @see `BuildingInfoUpdateMessage` * @see `DensityLevelUpdateMessage` * @param continentNumber the zone id @@ -7411,13 +7193,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con boomers.foreach(boomer => { continent.GUID(boomer) match { case Some(obj: BoomerDeployable) => - obj.OwnerName = None - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) + obj.Actor ! Deployable.Ownership(None) case Some(_) | None => ; } }) - val triggers = RemoveBoomerTriggersFromInventory() - triggers.foreach(trigger => { NormalItemDrop(obj, continent)(trigger) }) + RemoveBoomerTriggersFromInventory()foreach(trigger => { NormalItemDrop(obj, continent)(trigger) }) } } @@ -8009,11 +7789,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.info(s"${player.Name} is attacking ${obj.OwnerName.getOrElse("someone")}'s ${obj.Definition.Name}") obj.Actor ! Vitality.Damage(func) case obj: Amenity if obj.CanDamage => obj.Actor ! Vitality.Damage(func) - case obj: ComplexDeployable if obj.CanDamage => obj.Actor ! Vitality.Damage(func) - - case obj: SimpleDeployable if obj.CanDamage => - //damage is synchronized on `LSA` (results returned to and distributed from this `WorldSessionActor`) - continent.LocalEvents ! Vitality.DamageOn(obj, func) + case obj: Deployable if obj.CanDamage => obj.Actor ! Vitality.Damage(func) case _ => ; } } @@ -8104,7 +7880,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @see `SetCurrentAvatar` * @param obj a `Deployable` object */ - def RedrawDeployableIcons(obj: PlanetSideGameObject with Deployable): Unit = { + def RedrawDeployableIcons(obj: Deployable): Unit = { val deployInfo = DeployableInfo( obj.GUID, Deployable.Icon(obj.Definition.Item), @@ -8135,269 +7911,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @see `SetCurrentAvatar` * @param obj a `Deployable` object */ - def DontRedrawIcons(obj: PlanetSideGameObject with Deployable): Unit = {} - - /** - * 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 `FireModeSwitch.NextFireMode` - * @see `PerformConstructionItemAmmoChange` - * @param obj the `ConstructionItem` object - * @param originalModeIndex the starting point fire mode index - * @return the changed fire mode - */ - def NextConstructionItemFireMode(obj: ConstructionItem, originalModeIndex: Int): ConstructionFireMode = { - do { - obj.NextFireMode - if (!Deployables.constructionItemPermissionComparison(player.avatar.certifications, obj.ModePermissions)) { - PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex) - } - sendResponse(ChangeFireModeMessage(obj.GUID, obj.FireModeIndex)) - } while (!Deployables.constructionItemPermissionComparison( - player.avatar.certifications, - obj.ModePermissions - ) && originalModeIndex != obj.FireModeIndex) - obj.FireMode - } - - /** - * The custom behavior responding to the message `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. - * @param obj the `ConstructionItem` object - * @param originalAmmoIndex the starting point ammunition type mode index - */ - def PerformConstructionItemAmmoChange(obj: ConstructionItem, originalAmmoIndex: Int): Unit = { - do { - obj.NextAmmoType - } while (!Deployables.constructionItemPermissionComparison( - player.avatar.certifications, - obj.ModePermissions - ) && originalAmmoIndex != obj.AmmoTypeIndex) - log.info( - s"${player.Name} switched construction object ${obj.Definition.Name} to ${obj.AmmoType} (mode #${obj.FireModeIndex})" - ) - sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) - } + def DontRedrawIcons(obj: Deployable): Unit = {} /** * Common actions related to constructing a new `Deployable` object in the game environment.
*
- * Besides the standard `ObjectCreateMessage` packet that produces the model and game object on the client, - * two messages are dispatched in accordance with enforced deployable limits. - * The first limit of note is the actual number of a specific type of deployable can be placed. - * The second limit of note is the actual number of a specific group (category) of deployables that can be placed. - * For example, the player can place 25 mines but that count adds up all types of mines; - * specific mines have individual limits such as 25 and 5 and only that many of that type can be placed at once. - * Depending on which limit is encountered, an "oldest entry" is struck from the list to make space. - * This generates the first message - "@*OldestDestroyed." - * The other 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."
- *
* The map icon for the deployable just introduced is also created on the clients of all faction-affiliated players. * This icon is important as, short of destroying it, * the owner has no other means of controlling the created object that it is associated with. * @param obj the `Deployable` object to be built */ - def DeployableBuildActivity(obj: PlanetSideGameObject with Deployable): Unit = { - val guid = obj.GUID - val definition = obj.Definition - val item = definition.Item - val deployables = avatar.deployables - val (curr, max) = deployables.CountDeployable(item) - //two potential messages related to numerical limitations of deployables - if (!avatar.deployables.Available(obj)) { - val (removed, msg) = { - if (curr == max) { //too many of a specific type of deployable - (deployables.DisplaceFirst(obj), max > 1) - } 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.AssignOwnership(None) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), continent)) - continent.LocalEvents ! LocalServiceMessage.Deployables( - RemoverActor.AddTask(telepad, continent, Some(0 seconds)) - ) //normal decay - case Some(old) => - old.AssignOwnership(None) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(old), continent)) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(old, continent, Some(0 seconds))) - if (msg) { //max test - sendResponse( - ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None) - ) - } - case None => ; //should be an invalid case - log.warn( - s"DeployableBuildActivity: how awkward: ${player.Name} probably shouldn't be allowed to build this deployable right now" - ) - } - } else if (obj.isInstanceOf[TelepadDeployable]) { - //always treat the telepad we are putting down as the first and only one - sendResponse(ObjectDeployedMessage.Success(definition.Name, 1, 1)) - } else { - sendResponse(ObjectDeployedMessage.Success(definition.Name, curr + 1, max)) - val (catCurr, catMax) = deployables.CountCategory(item) - if ((max > 1 && curr + 1 == max) || (catMax > 1 && catCurr + 1 == catMax)) { - sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}LimitReached", None)) - } - } - avatar.deployables.Add(obj) - UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) - sendResponse(GenericObjectActionMessage(guid, 21)) //reset build cooldown - sendResponse(ObjectCreateMessage(definition.ObjectId, guid, definition.Packet.ConstructorData(obj).get)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.DeployItem(player.GUID, obj)) - //map icon - val deployInfo = DeployableInfo(guid, Deployable.Icon(item), obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0))) - sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) - continent.LocalEvents ! LocalServiceMessage( - s"${player.Faction}", - LocalAction.DeployableMapIcon(player.GUID, DeploymentAction.Build, deployInfo) - ) - } - - /** - * If the tool is a form of field deployment unit (FDU, also called an `advanced_ace`), - * completely remove the object from its current position and place it on the ground. - * In the case of a botched deployable construction, dropping the FDU is visually consistent - * as it should already be depicted as on the ground as a part of its animation cycle. - * @param tool the `ConstructionItem` object currently in the slot (checked) - * @param index the slot index - * @param pos where to drop the object in the game world - */ - def TryDropFDU(tool: ConstructionItem, index: Int, pos: Vector3): Unit = { - if (tool.Definition == GlobalDefinitions.advanced_ace) { - DropEquipmentFromInventory(player)(tool, Some(pos)) - } - } - - /** - * 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(tool: ConstructionItem, index: Int): Unit = { - if (SafelyRemoveConstructionItemFromSlot(tool, index, "CommonDestroyConstructionItem")) { - continent.tasks ! GUIDTask.UnregisterEquipment(tool)(continent.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.
- *
- * 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( - 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 - } - } - }) { - sendResponse(ObjectDeleteMessage(tool.GUID, 0)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(player.GUID, tool.GUID)) - 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.
- *
- * 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`
- * `FindEquipmentStock` - * @param tool the `ConstructionItem` object to match - * @param index where to put the discovered replacement - */ - def FindReplacementConstructionItem(tool: ConstructionItem, index: Int): Unit = { - val fireMode = tool.FireModeIndex - val ammoType = tool.AmmoTypeIndex - val definition = tool.Definition - - if (player.Slot(index).Equipment.isEmpty) { - FindEquipmentStock(player, { e => e.Definition == definition }, 1) match { - case x :: _ => - val guid = player.GUID - val obj = x.obj.asInstanceOf[ConstructionItem] - if ((player.Slot(index).Equipment = obj).contains(obj)) { - player.Inventory -= x.start - sendResponse(ObjectAttachMessage(guid, obj.GUID, index)) - - if (obj.FireModeIndex != fireMode) { - obj.FireModeIndex = fireMode - sendResponse(ChangeFireModeMessage(obj.GUID, fireMode)) - } - if (obj.AmmoTypeIndex != ammoType) { - obj.AmmoTypeIndex = ammoType - sendResponse(ChangeAmmoMessage(obj.GUID, ammoType)) - } - if (player.VisibleSlots.contains(index)) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.EquipmentInHand(guid, guid, index, obj) - ) - if (player.DrawnSlot == Player.HandsDownSlot) { - player.DrawnSlot = index - sendResponse(ObjectHeldMessage(guid, index, false)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectHeld(guid, 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" - ) - } + def DeployableBuildActivity(obj: Deployable): Unit = { + sendResponse(GenericObjectActionMessage(obj.GUID, 21)) //reset build cooldown + UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(obj.Definition.Item)) } /** @@ -8406,8 +7932,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * the player's locker inventory will be checked, and then * the game environment (items on the ground) will be checked too. * If the target object is discovered, it is removed from its current location and is completely destroyed. - * @see `RequestDestroyMessage`
- * `Zone.ItemIs.Where` + * @see `RequestDestroyMessage` + * @see `Zone.ItemIs.Where` * @param object_guid the target object's globally unique identifier; * it is not expected that the object will be unregistered, but it is also not gauranteed * @param obj the target object @@ -8453,25 +7979,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - /** - * Common behavior for deconstructing expended explosive deployables in the game environment. - * @param obj the deployable - * @param guid the globally unique identifier for the deployable - * @param pos the previous position of the deployable - */ - def DeconstructDeployable(obj: PlanetSideGameObject with Deployable, guid: PlanetSideGUID, pos: Vector3): Unit = { - sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) //for some, removes the green marker circle - sendResponse(ObjectDeleteMessage(guid, 0)) - if (player.Faction == obj.Faction) { - sendResponse( - DeployableObjectsInfoMessage( - DeploymentAction.Dismiss, - DeployableInfo(guid, Deployable.Icon(obj.Definition.Item), pos, obj.Owner.getOrElse(PlanetSideGUID(0))) - ) - ) - } - } - /** * Common behavior for deconstructing deployables in the game environment. * @param obj the deployable @@ -8481,24 +7988,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation */ def DeconstructDeployable( - obj: PlanetSideGameObject with Deployable, + obj: Deployable, guid: PlanetSideGUID, pos: Vector3, orient: Vector3, deletionType: Int ): Unit = { - sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) //for some, removes the green marker circle sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient)) sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish sendResponse(ObjectDeleteMessage(guid, deletionType)) - if (player.Faction == obj.Faction) { - sendResponse( - DeployableObjectsInfoMessage( - DeploymentAction.Dismiss, - DeployableInfo(guid, Deployable.Icon(obj.Definition.Item), pos, obj.Owner.getOrElse(PlanetSideGUID(0))) - ) - ) - } } /** @@ -8510,17 +8008,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val events = continent.AvatarEvents val zoneId = continent.id (player.Inventory.Items ++ player.HolsterItems()) - .collect { - case InventoryItem(obj: BoomerTrigger, index) => - player.Slot(index).Equipment = None + .collect { case InventoryItem(obj: BoomerTrigger, index) => + player.Slot(index).Equipment = None + continent.GUID(obj.Companion) match { + case Some(mine: BoomerDeployable) => mine.Actor ! Deployable.Ownership(None) + case _ => ; + } + if (player.VisibleSlots.contains(index)) { + events ! AvatarServiceMessage( + zoneId, + AvatarAction.ObjectDelete(Service.defaultPlayerGUID, obj.GUID) + ) + } else { sendResponse(ObjectDeleteMessage(obj.GUID, 0)) - if (player.HasGUID && player.VisibleSlots.contains(index)) { - events ! AvatarServiceMessage( - zoneId, - AvatarAction.ObjectDelete(player.GUID, obj.GUID) - ) - } - obj + } + obj } } @@ -8914,7 +8416,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def ToggleTeleportSystem(router: Vehicle, systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)]): Unit = { systemPlan match { case Some((internalTelepad, remoteTelepad)) => - LinkRouterToRemoteTelepad(router, internalTelepad, remoteTelepad) + internalTelepad.Telepad = remoteTelepad.GUID //necessary; backwards link to the (new) telepad + TelepadLike.StartRouterInternalTelepad(continent, router.GUID, internalTelepad) + TelepadLike.LinkTelepad(continent, remoteTelepad.GUID) case _ => router.Utility(UtilityType.internal_router_telepad_deployable) match { case Some(util: Utility.InternalTelepad) => @@ -8924,69 +8428,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - /** - * Link the router teleport system using the provided terminal information. - * The internal telepad is made known of the remote telepad, creating the link. - * @param router the vehicle that houses one end of the teleportation system (the `internalTelepad`) - * @param internalTelepad the endpoint of the teleportation system housed by the router - * @param remoteTelepad the endpoint of the teleportation system that exists in the environment - */ - def LinkRouterToRemoteTelepad( - router: Vehicle, - internalTelepad: Utility.InternalTelepad, - remoteTelepad: TelepadDeployable - ): Unit = { - internalTelepad.Telepad = remoteTelepad.GUID //necessary; backwards link to the (new) telepad - CreateRouterInternalTelepad(router, internalTelepad) - LinkRemoteTelepad(remoteTelepad.GUID) - } - - /** - * Create the mechanism that serves as one endpoint of the linked router teleportation system.
- *
- * 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 router the vehicle that houses one end of the teleportation system (the `internalTelepad`) - * @param internalTelepad the endpoint of the teleportation system housed by the router - */ - def CreateRouterInternalTelepad(router: Vehicle, internalTelepad: PlanetSideGameObject with TelepadLike): Unit = { - //create the interal telepad each time the link is made - val rguid = router.GUID - val uguid = internalTelepad.GUID - val udef = internalTelepad.Definition - /* - 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 - */ - sendResponse( - ObjectCreateMessage( - udef.ObjectId, - uguid, - ObjectCreateMessageParent(rguid, 2), //TODO stop assuming slot number - udef.Packet.ConstructorData(internalTelepad).get - ) - ) - sendResponse(GenericObjectActionMessage(uguid, 27)) - sendResponse(GenericObjectActionMessage(uguid, 30)) - /* - the following configurations create the interactive beam underneath the Deployed Router - normally dispatched after the warm-up timer has completed - */ - sendResponse(GenericObjectActionMessage(uguid, 27)) - sendResponse(GenericObjectActionMessage(uguid, 28)) - } - - /** - * na - * @param telepadGUID na - */ - def LinkRemoteTelepad(telepadGUID: PlanetSideGUID): Unit = { - sendResponse(GenericObjectActionMessage(telepadGUID, 27)) - sendResponse(GenericObjectActionMessage(telepadGUID, 28)) - } - /** * A player uses a fully-linked Router teleportation system. * @param router the Router vehicle diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index 9b8546df..55762314 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -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) diff --git a/src/main/scala/net/psforever/login/WorldSession.scala b/src/main/scala/net/psforever/login/WorldSession.scala index 1a6b665b..a7bd88b2 100644 --- a/src/main/scala/net/psforever/login/WorldSession.scala +++ b/src/main/scala/net/psforever/login/WorldSession.scala @@ -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) + ) + } } diff --git a/src/main/scala/net/psforever/objects/BoomerDeployable.scala b/src/main/scala/net/psforever/objects/BoomerDeployable.scala index 42b29399..b4fc98f3 100644 --- a/src/main/scala/net/psforever/objects/BoomerDeployable.scala +++ b/src/main/scala/net/psforever/objects/BoomerDeployable.scala @@ -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 => ; + } + } +} diff --git a/src/main/scala/net/psforever/objects/ConstructionItem.scala b/src/main/scala/net/psforever/objects/ConstructionItem.scala index 2f779c24..220c6f8b 100644 --- a/src/main/scala/net/psforever/objects/ConstructionItem.scala +++ b/src/main/scala/net/psforever/objects/ConstructionItem.scala @@ -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) diff --git a/src/main/scala/net/psforever/objects/Deployables.scala b/src/main/scala/net/psforever/objects/Deployables.scala index 3799c18c..8e5b9d33 100644 --- a/src/main/scala/net/psforever/objects/Deployables.scala +++ b/src/main/scala/net/psforever/objects/Deployables.scala @@ -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. diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 381ffd5f..00322346 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -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 _ => ; } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 2d538571..7db52cd5 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 3f4a71f5..e86d576f 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -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 { diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala index 2eaac667..9317937a 100644 --- a/src/main/scala/net/psforever/objects/Players.scala +++ b/src/main/scala/net/psforever/objects/Players.scala @@ -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.
+ *
+ * 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."
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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`
+ * `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" + ) + } + } } diff --git a/src/main/scala/net/psforever/objects/SensorDeployable.scala b/src/main/scala/net/psforever/objects/SensorDeployable.scala index 24e268d9..ff6d5a91 100644 --- a/src/main/scala/net/psforever/objects/SensorDeployable.scala +++ b/src/main/scala/net/psforever/objects/SensorDeployable.scala @@ -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( diff --git a/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala b/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala index d8b8f8bb..2206eda8 100644 --- a/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala +++ b/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala @@ -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) } } diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala index c0b8a1a2..17769771 100644 --- a/src/main/scala/net/psforever/objects/SpecialEmp.scala +++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala @@ -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 } } -} \ No newline at end of file +} diff --git a/src/main/scala/net/psforever/objects/Telepad.scala b/src/main/scala/net/psforever/objects/Telepad.scala index 956230e0..4ced4598 100644 --- a/src/main/scala/net/psforever/objects/Telepad.scala +++ b/src/main/scala/net/psforever/objects/Telepad.scala @@ -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 = { diff --git a/src/main/scala/net/psforever/objects/TelepadDeployable.scala b/src/main/scala/net/psforever/objects/TelepadDeployable.scala index 89517362..1a00dba9 100644 --- a/src/main/scala/net/psforever/objects/TelepadDeployable.scala +++ b/src/main/scala/net/psforever/objects/TelepadDeployable.scala @@ -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)) + } +} diff --git a/src/main/scala/net/psforever/objects/TrapDeployable.scala b/src/main/scala/net/psforever/objects/TrapDeployable.scala index 6a9ec388..f4203823 100644 --- a/src/main/scala/net/psforever/objects/TrapDeployable.scala +++ b/src/main/scala/net/psforever/objects/TrapDeployable.scala @@ -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 _ => diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 76f4eabb..b2616885 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -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) + } } diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index c3cee443..eb9c659a 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -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. diff --git a/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala b/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala index 0683284a..91451b4e 100644 --- a/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala +++ b/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala @@ -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.)
+ *
* 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.
*
@@ -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. diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index d018b418..fd251119 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -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)) + } } diff --git a/src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala b/src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala deleted file mode 100644 index 8ee73d56..00000000 --- a/src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala +++ /dev/null @@ -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 - ) - } -} diff --git a/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala b/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala index 8e2f5d64..26a6fcdc 100644 --- a/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala +++ b/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala b/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala index eae56e98..820fe1ce 100644 --- a/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala +++ b/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala @@ -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) } } diff --git a/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala b/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala deleted file mode 100644 index 144925e5..00000000 --- a/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala +++ /dev/null @@ -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 -} diff --git a/src/main/scala/net/psforever/objects/ce/Deployable.scala b/src/main/scala/net/psforever/objects/ce/Deployable.scala index 844965e0..504450e0 100644 --- a/src/main/scala/net/psforever/objects/ce/Deployable.scala +++ b/src/main/scala/net/psforever/objects/ce/Deployable.scala @@ -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) diff --git a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala new file mode 100644 index 00000000..c7c4d151 --- /dev/null +++ b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala @@ -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.
+ *
+ * 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.
+ *
+ * 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() +} diff --git a/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala b/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala deleted file mode 100644 index 03cb1453..00000000 --- a/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala +++ /dev/null @@ -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 -} diff --git a/src/main/scala/net/psforever/objects/ce/TelepadLike.scala b/src/main/scala/net/psforever/objects/ce/TelepadLike.scala index 6ad62550..9f1ad124 100644 --- a/src/main/scala/net/psforever/objects/ce/TelepadLike.scala +++ b/src/main/scala/net/psforever/objects/ce/TelepadLike.scala @@ -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.
+ *
+ * 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 _ => ; } } diff --git a/src/main/scala/net/psforever/objects/definition/SimpleDeployableDefinition.scala b/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala similarity index 61% rename from src/main/scala/net/psforever/objects/definition/SimpleDeployableDefinition.scala rename to src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala index 89810d07..78ffb641 100644 --- a/src/main/scala/net/psforever/objects/definition/SimpleDeployableDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala @@ -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 - } -} diff --git a/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala index 5ad1b408..264beb5b 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala @@ -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")) } diff --git a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 596bf5c8..8ee353db 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala b/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala index b39cd262..32bc75d6 100644 --- a/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala +++ b/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala @@ -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.
+ *
+ * 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] { diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 368fa062..7d255c7f 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -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 _ => ; } } diff --git a/src/main/scala/net/psforever/objects/vital/Vitality.scala b/src/main/scala/net/psforever/objects/vital/Vitality.scala index c31aa88d..6747727d 100644 --- a/src/main/scala/net/psforever/objects/vital/Vitality.scala +++ b/src/main/scala/net/psforever/objects/vital/Vitality.scala @@ -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) } diff --git a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index c8e83fed..be9672e6 100644 --- a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 3feab123..4eb851d4 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -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 } } /** diff --git a/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala index 8192c7e2..e946d0fc 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala @@ -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) { diff --git a/src/main/scala/net/psforever/packet/control/Unknown30.scala b/src/main/scala/net/psforever/packet/control/Unknown30.scala index a7ed734b..16839108 100644 --- a/src/main/scala/net/psforever/packet/control/Unknown30.scala +++ b/src/main/scala/net/psforever/packet/control/Unknown30.scala @@ -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] } diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedConstructionToolData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedConstructionToolData.scala index 7413d8df..f376f5d3 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedConstructionToolData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedConstructionToolData.scala @@ -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 diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index 589fb6ee..3863a806 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -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) diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index e475a068..c36d7f56 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -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) diff --git a/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala b/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala index 2f2eeb07..188c6526 100644 --- a/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala +++ b/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala @@ -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 { diff --git a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala index 6f369bdb..d18c2437 100644 --- a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala +++ b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala @@ -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) diff --git a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala index ceebb9d4..6a2c7f04 100644 --- a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala +++ b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala @@ -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 diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 8214bfe1..4c9b5606 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -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.
- *
- * 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 => ; - } - } } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index 0eb988fb..db5a7ab0 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -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, diff --git a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala index 48626712..2aadb269 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala @@ -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)] diff --git a/src/main/scala/net/psforever/services/local/support/DeployableRemover.scala b/src/main/scala/net/psforever/services/local/support/DeployableRemover.scala deleted file mode 100644 index 0d8f1e65..00000000 --- a/src/main/scala/net/psforever/services/local/support/DeployableRemover.scala +++ /dev/null @@ -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) -} diff --git a/src/main/scala/net/psforever/services/local/support/RouterTelepadActivation.scala b/src/main/scala/net/psforever/services/local/support/RouterTelepadActivation.scala index decf9407..e070da9d 100644 --- a/src/main/scala/net/psforever/services/local/support/RouterTelepadActivation.scala +++ b/src/main/scala/net/psforever/services/local/support/RouterTelepadActivation.scala @@ -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) } diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 53a9ce57..7134cd53 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -785,8 +785,6 @@ object Zones { 60 seconds case _: DeployableSource => 60 seconds - case _: ComplexDeployableSource => - 60 seconds case _ => 0 seconds } diff --git a/src/test/scala/objects/DeployableBehaviorTest.scala b/src/test/scala/objects/DeployableBehaviorTest.scala new file mode 100644 index 00000000..1cce70a7 --- /dev/null +++ b/src/test/scala/objects/DeployableBehaviorTest.scala @@ -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 { + //... +} diff --git a/src/test/scala/objects/DeployableTest.scala b/src/test/scala/objects/DeployableTest.scala index f8eca34b..e85471f8 100644 --- a/src/test/scala/objects/DeployableTest.scala +++ b/src/test/scala/objects/DeployableTest.scala @@ -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) } diff --git a/src/test/scala/objects/EquipmentTest.scala b/src/test/scala/objects/EquipmentTest.scala index 8c8bdf0c..9cb82776 100644 --- a/src/test/scala/objects/EquipmentTest.scala +++ b/src/test/scala/objects/EquipmentTest.scala @@ -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 { diff --git a/src/test/scala/objects/TelepadRouterTest.scala b/src/test/scala/objects/TelepadRouterTest.scala new file mode 100644 index 00000000..683f5e37 --- /dev/null +++ b/src/test/scala/objects/TelepadRouterTest.scala @@ -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 + } +} diff --git a/src/test/scala/service/LocalServiceTest.scala b/src/test/scala/service/LocalServiceTest.scala index fe03619c..774cc2d4 100644 --- a/src/test/scala/service/LocalServiceTest.scala +++ b/src/test/scala/service/LocalServiceTest.scala @@ -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) diff --git a/src/test/scala/service/RouterTelepadActivationTest.scala b/src/test/scala/service/RouterTelepadActivationTest.scala deleted file mode 100644 index e5b21f4a..00000000 --- a/src/test/scala/service/RouterTelepadActivationTest.scala +++ /dev/null @@ -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) - } - } -}