diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala index bdda54dab..042a3551e 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -3,6 +3,7 @@ package net.psforever.actors.session.normal import akka.actor.{ActorContext, typed} import net.psforever.actors.session.support.AvatarHandlerFunctions +import net.psforever.objects.serverobject.containable.ContainableBehavior import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction} import net.psforever.types.ImplantType @@ -295,6 +296,17 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0)) } //functionally delete + if (delete.size > 1 || delete.nonEmpty && !delete.exists { + case (e: Tool, _) => GlobalDefinitions.isMaxArms(e.Definition) + case _ => false + }) { + /* + if going x -> max, you will have enough space in max inventory for any displaced holster equipment + for max -> max, don't care about the max weapon arm being deleted (allow for 1) + for any other x -> x, any deleted equipment will raise this comment + */ + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@ItemsDeconstructed")) + } delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) } //redraw if (maxhand) { @@ -330,13 +342,17 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A } DropLeftovers(player)(drop) - case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, _, delete) => + case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, drop, delete) => sendResponse(ArmorChangedMessage(target, exosuit, subtype)) sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) //happening to some other player sendResponse(ObjectHeldMessage(target, slot, unk1 = false)) //cleanup - (oldHolsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) } + val dropPred = ContainableBehavior.DropPredicate(player) + val deleteFromDrop = drop.filterNot(dropPred) + (oldHolsters ++ delete ++ deleteFromDrop.map(f =>(f.obj, f.GUID))) + .distinctBy(_._2) + .foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) } //draw holsters holsters.foreach { case InventoryItem(obj, index) => diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala index dc599e017..e701c0a55 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -145,8 +145,11 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_KICK, _, contents) if gmCommandAllowed => ops.commandKick(session, message, contents) + case (CMT_REPORTUSER, _, contents) => + ops.commandReportUser(session, message, contents) + case _ => - log.warn(s"Unhandled chat message $message") + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@no_permission")) } } diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index 1eb457f9d..ab40f6222 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -162,6 +162,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } case None => () } + //llu destruction check + if (player.Carrying.contains(SpecialCarry.CaptureFlag)) { + CaptureFlagManager.ReasonToLoseFlagViolently(continent, sessionLogic.general.specialItemSlotGuid, player) + } + // val eagleEye: Boolean = ops.canSeeReallyFar val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) diff --git a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala index 211cc48ef..c2f1a8969 100644 --- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala @@ -11,6 +11,7 @@ import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior} @@ -19,7 +20,7 @@ import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleC import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} +import net.psforever.types.{BailType, ChatMessageType, DriveState, PlanetSideGUID, Vector3} import scala.concurrent.duration._ @@ -71,18 +72,6 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act obj.Actor ! Mountable.TryDismount(player, seat_num, bailType) //short-circuit the temporary channel for transferring between zones, the player is no longer doing that sessionLogic.zoning.interstellarFerry = None - // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight - //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle - //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. - //todo: kick cargo passengers out. To be added after PR #216 is merged - obj match { - case v: Vehicle - if bailType == BailType.Bailed && - v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) && - v.isFlying => - v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction - case _ => () - } case None => dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player) @@ -368,18 +357,19 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act DismountAction(tplayer, obj, seatNum) obj.Actor ! Vehicle.Deconstruct() + case Mountable.CanDismount(obj: Vehicle, seatNum, _) + if tplayer.GUID == player.GUID && + obj.isFlying && + obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) => + // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight + //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle + //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. + //todo: kick cargo passengers out. To be added after PR #216 is merged + DismountVehicleAction(tplayer, obj, seatNum) + obj.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction + case Mountable.CanDismount(obj: Vehicle, seatNum, _) if tplayer.GUID == player.GUID => - //disembarking self - log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${ - obj.SeatPermissionGroup(seatNum) match { - case Some(AccessPermissionGroup.Driver) => "driver seat" - case Some(seatType) => s"$seatType seat (#$seatNum)" - case None => "seat" - } - }") - sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) - sessionLogic.general.unaccessContainer(obj) DismountVehicleAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Vehicle, seat_num, _) => @@ -388,7 +378,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID) ) - case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => + case Mountable.CanDismount(obj: PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}") DismountAction(tplayer, obj, seatNum) @@ -407,7 +397,38 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanNotMount(obj: Mountable, seatNumber) => log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed") - case Mountable.CanNotDismount(obj, seatNum) => + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Normal) + if obj.DeploymentState == DriveState.AutoPilot => + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotDismountAtThisTime")) + + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if obj.Definition == GlobalDefinitions.droppod => + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@CannotBailFromDroppod")) + + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if obj.DeploymentState == DriveState.AutoPilot => + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime")) + + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if { + continent + .blockMap + .sector(obj) + .buildingList + .exists { + case wg: WarpGate => + Vector3.DistanceSquared(obj.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2) + case _ => + false + } + } => + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@Vehicle_CannotBailInWarpgateEnvelope")) + + case Mountable.CanNotDismount(obj: Vehicle, _, _) + if obj.isMoving(test = 1f) => + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@TooFastToDismount")) + + case Mountable.CanNotDismount(obj, seatNum, _) => log.warn(s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seatNum, but was not allowed") } } @@ -465,7 +486,17 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act * @param obj the mountable object * @param seatNum the mount out of which which the player is disembarking */ - private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { + private def DismountVehicleAction(tplayer: Player, obj: Vehicle, seatNum: Int): Unit = { + //disembarking self + log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${ + obj.SeatPermissionGroup(seatNum) match { + case Some(AccessPermissionGroup.Driver) => "driver seat" + case Some(seatType) => s"$seatType seat (#$seatNum)" + case None => "seat" + } + }") + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) DismountAction(tplayer, obj, seatNum) //until vehicles maintain synchronized momentum without a driver obj match { diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala index 6d8f9c1a4..55d01d2a1 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala @@ -4,6 +4,7 @@ package net.psforever.actors.session.normal import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions} +import net.psforever.objects.avatar.SpecialCarry import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} @@ -12,6 +13,7 @@ import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage} import net.psforever.services.Service +import net.psforever.services.local.support.CaptureFlagManager import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse} import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} @@ -62,6 +64,14 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: player.Orientation = orient player.Velocity = vel sessionLogic.updateLocalBlockMap(pos) + //llu destruction check + if (player.Carrying.contains(SpecialCarry.CaptureFlag)) { + continent + .GUID(player.VehicleSeated) + .collect { case vehicle: Vehicle => + CaptureFlagManager.ReasonToLoseFlagViolently(continent, sessionLogic.general.specialItemSlotGuid, vehicle) + } + } case VehicleResponse.VehicleState( vehicleGuid, @@ -199,6 +209,9 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid)) sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid)) + case VehicleResponse.LoseOwnership(_, vehicleGuid) => + ops.announceAmsDecay(vehicleGuid,msg = "@ams_decaystarted") + case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget => sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue)) @@ -217,6 +230,16 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: case VehicleResponse.UnloadVehicle(_, vehicleGuid) => sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) + if (sessionLogic.zoning.spawn.prevSpawnPoint.map(_.Owner).exists { + case ams: Vehicle => + ams.GUID == vehicleGuid && + ams.OwnerGuid.isEmpty + case _ => + false + }) { + sessionLogic.zoning.spawn.prevSpawnPoint = None + sendResponse(ChatMsg(ChatMessageType.UNK_229, "@ams_decayed")) + } case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget => //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? @@ -308,13 +331,13 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle) case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) => - sendResponse(ChatMsg( - ChatMessageType.CMT_OPEN, - wideContents=true, - recipient="", - s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}", - note=None - )) + val str = s"${data.getOrElse("The vehicle spawn pad where you placed your order is blocked.")}" + val msg = if (str.contains("@")) { + ChatMsg(ChatMessageType.UNK_229, str) + } else { + ChatMsg(ChatMessageType.CMT_OPEN, wideContents = true, recipient = "", str, note = None) + } + sendResponse(msg) case VehicleResponse.PeriodicReminder(_, data) => val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match { diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala index 6b293ed75..5b8ab62d1 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -10,9 +10,9 @@ import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} +import net.psforever.packet.game.{ChatMsg, ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{DriveState, Vector3} +import net.psforever.types.{ChatMessageType, DriveState, Vector3} object VehicleLogic { def apply(ops: VehicleOperations): VehicleLogic = { @@ -70,6 +70,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Velocity = None obj.Flying = None } + // continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.VehicleState( @@ -303,6 +304,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex log.trace(s"DeployRequest: $obj transitioning to deploy state") } else if (state == DriveState.Deployed) { log.trace(s"DeployRequest: $obj has been Deployed") + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@DeployingMessage")) } else { CanNotChangeDeployment(obj, state, "incorrect deploy state") } @@ -313,6 +315,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex log.trace(s"DeployRequest: $obj transitioning to undeploy state") } else if (state == DriveState.Mobile) { log.trace(s"DeployRequest: $obj is Mobile") + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@UndeployingMessage")) } else { CanNotChangeDeployment(obj, state, "incorrect undeploy state") } diff --git a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala index 3b7d10edd..a7e5884e2 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -85,7 +85,11 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_WARP, _, contents) => ops.commandWarp(session, message, contents) - case _ => () + case (CMT_REPORTUSER, _, contents) => + ops.commandReportUser(session, message, contents) + + case _ => + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@no_permission")) } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala index f6c3a4a43..2cf441e0f 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala @@ -203,9 +203,9 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => DismountAction(tplayer, obj, seatNum) - case Mountable.CanDismount(obj: Mountable, _, _) => () + case Mountable.CanDismount(_: Mountable, _, _) => () - case Mountable.CanNotDismount(obj: Vehicle, seatNum) => + case Mountable.CanNotDismount(obj: Vehicle, _, _) => obj.Actor ! Vehicle.Deconstruct() case _ => () diff --git a/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala index 10fdb8ed6..7ba9cca0b 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala @@ -279,11 +279,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { /* intentionally blank */ } - def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { - if (state != DriveState.Undeploying && state != DriveState.Mobile) { - CanNotChangeDeployment(obj, state, "incorrect undeploy state") - } - } + def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { /* intentionally blank */ } def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = { if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) { diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index 412a4c343..02efbc7e4 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -91,11 +91,13 @@ class ChatOperations( } def commandWatermark(contents: String): Unit = { - val connectionState = + val connectionState = { if (contents.contains("40 80")) 100 else if (contents.contains("120 200")) 25 else 50 + } context.self ! SessionActor.SetConnectionState(connectionState) + context.self ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_227, "@CMT_CULLWATERMARK_success")) } def commandSpeed(message: ChatMsg, contents: String): Unit = { @@ -647,6 +649,11 @@ class ChatOperations( } } + def commandReportUser(@unused session: Session, @unused message: ChatMsg, @unused contents: String): Unit = { + //todo get user from contents + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@rpt_i")) + } + def commandIncomingSendAllIfOnline(session: Session, message: ChatMsg): Unit = { if (AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)) { sendResponse(message) diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index deea975f9..67bf09d4e 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -220,8 +220,12 @@ class GeneralOperations( ) } } - continent.LocalEvents ! CaptureFlagManager.DropFlag(llu) - case Some((llu, Some(carrier: Player))) if carrier.GUID == player.GUID => + if (!CaptureFlagManager.ReasonToLoseFlagViolently(continent, Some(guid), player)) { + continent.LocalEvents ! CaptureFlagManager.DropFlag(llu) + } + case Some((llu, Some(carrier: Player))) + if carrier.GUID == player.GUID && + !CaptureFlagManager.ReasonToLoseFlagViolently(continent, Some(guid), player) => continent.LocalEvents ! CaptureFlagManager.DropFlag(llu) case Some((_, Some(carrier: Player))) => log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}") @@ -669,6 +673,9 @@ class GeneralOperations( case _ => () } } else { + if (player.Capacitor < 1f && player.UsingSpecial == SpecialExoSuitDefinition.Mode.Shielded) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@ArmorShieldOff")) + } player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal continent.AvatarEvents ! AvatarServiceMessage( continent.id, diff --git a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala index 5389b6bb9..b493b1ff2 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala @@ -3,8 +3,10 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.AvatarActor +import net.psforever.objects.Vehicle +import net.psforever.packet.game.ChatMsg import net.psforever.services.vehicle.VehicleResponse -import net.psforever.types.PlanetSideGUID +import net.psforever.types.{ChatMessageType, DriveState, PlanetSideGUID} trait VehicleHandlerFunctions extends CommonSessionInterfacingFunctionality { def ops: SessionVehicleHandlers @@ -17,4 +19,17 @@ class SessionVehicleHandlers( val avatarActor: typed.ActorRef[AvatarActor.Command], val galaxyService: ActorRef, implicit val context: ActorContext - ) extends CommonSessionInterfacingFunctionality + ) extends CommonSessionInterfacingFunctionality { + def announceAmsDecay(vehicleGuid: PlanetSideGUID, msg: String): Unit = { + if (sessionLogic.zoning.spawn.prevSpawnPoint.map(_.Owner).exists { + case ams: Vehicle => + ams.GUID == vehicleGuid && + ams.DeploymentState == DriveState.Deployed && + ams.OwnerGuid.isEmpty + case _ => + false + }) { + sendResponse(ChatMsg(ChatMessageType.UNK_229, msg)) + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 7741fa86f..55753dff9 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -2,9 +2,11 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} +import net.psforever.objects.definition.SpecialExoSuitDefinition import net.psforever.objects.zones.Zoning import net.psforever.objects.serverobject.turret.VanuSentry import net.psforever.objects.zones.exp.ToDatabase +import net.psforever.types.ChatMessageType import scala.collection.mutable import scala.concurrent.duration._ @@ -83,9 +85,10 @@ class WeaponAndProjectileOperations( if (player.ZoningRequest != Zoning.Method.None) { sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_fire") } - if (player.isShielded) { + if (player.UsingSpecial == SpecialExoSuitDefinition.Mode.Shielded) { // Cancel NC MAX shield if it's active sessionLogic.general.toggleMaxSpecialState(enable = false) + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@ArmorShieldOverride")) } val (o, tools) = FindContainedWeapon val (_, enabledTools) = FindEnabledWeaponsToHandleWeaponFireAccountability(o, tools) diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index ebeb512fc..4af23102d 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -1792,6 +1792,7 @@ class ZoningOperations( private[session] var setupAvatarFunc: () => Unit = AvatarCreate private[session] var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally private[session] var nextSpawnPoint: Option[SpawnPoint] = None + private[session] var prevSpawnPoint: Option[SpawnPoint] = None private[session] var interimUngunnedVehicle: Option[PlanetSideGUID] = None private[session] var interimUngunnedVehicleSeat: Option[Int] = None /** Upstream message counter
@@ -2811,6 +2812,7 @@ class ZoningOperations( ) ) nextSpawnPoint = physSpawnPoint + prevSpawnPoint = physSpawnPoint shiftPosition = Some(pos) shiftOrientation = Some(ori) val toZoneNumber = if (continent.id.equals(zoneId)) { @@ -2819,10 +2821,14 @@ class ZoningOperations( Zones.zones.find { _.id.equals(zoneId) }.orElse(Some(Zone.Nowhere)).get.Number } val toSide = physSpawnPoint.map(_.Owner) match { - case Some(_: WarpGate) => Sidedness.OutsideOf - case Some(_: Building) => Sidedness.InsideOf - case Some(v: Vehicle) => v.WhichSide //though usually OutsideOf - case _ => Sidedness.StrictlyBetweenSides //todo needs better determination + case Some(_: WarpGate) => + Sidedness.OutsideOf + case Some(_: Building) => + Sidedness.InsideOf + case Some(v: Vehicle) => + v.WhichSide //though usually OutsideOf + case _ => + Sidedness.StrictlyBetweenSides //todo needs better determination } val toSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) } respawnTimer = context.system.scheduler.scheduleOnce(respawnTime) { @@ -3124,11 +3130,17 @@ class ZoningOperations( Config.app.game.savedMsg.short.fixed, Config.app.game.savedMsg.short.variable ) - val effortBy = nextSpawnPoint + val effortBy = prevSpawnPoint .collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) } .collect { - case (_, Some(v: Vehicle)) => continent.GUID(v.OwnerGuid) - case (sp, Some(_: Building)) => Some(sp) + case (_, Some(v: Vehicle)) => + sessionLogic.vehicleResponseOperations.announceAmsDecay( + v.GUID, + msg = "The AMS you were bound to has lost its' owner. It will auto-deconstruct soon." + ) + continent.GUID(v.OwnerGuid) + case (sp, Some(_: Building)) => + Some(sp) } .collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) } .flatten diff --git a/src/main/scala/net/psforever/login/WorldSession.scala b/src/main/scala/net/psforever/login/WorldSession.scala index ae02c6f64..a83a9df00 100644 --- a/src/main/scala/net/psforever/login/WorldSession.scala +++ b/src/main/scala/net/psforever/login/WorldSession.scala @@ -65,7 +65,7 @@ object WorldSession { Zone.Ground.DropItem(localItem, localContainer.Position, Vector3.z(localContainer.Orientation.z)), localContainer.Actor ) - case _ => ; + case _ => () } result } @@ -163,7 +163,7 @@ object WorldSession { result.onComplete { case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(localContainer.Zone.GUID, localItem)) - case _ => ; + case _ => () } result } @@ -361,17 +361,21 @@ object WorldSession { val future = ask(localZone.Ground, Zone.Ground.PickupItem(item.GUID)) future.onComplete { case Success(Zone.Ground.ItemInHand(_)) => - PutEquipmentInInventoryOrDrop(localContainer)(localItem) + PutEquipmentInInventoryOrDrop(localContainer)(localItem).onComplete { + case Success(Containable.ItemPutInSlot(_, _, Player.FreeHandSlot, _)) => + localContainer.Actor ! Zone.Ground.CanNotPickupItem(localZone, localItem.GUID, "@InventoryPickupNoRoom") + case _ => () + } case Success(Zone.Ground.CanNotPickupItem(_, item_guid, _)) => localZone.GUID(item_guid) match { - case Some(_) => ; + case Some(_) => () case None => //acting on old data? localZone.AvatarEvents ! AvatarServiceMessage( localZone.id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item_guid) ) } - case _ => ; + case _ => () } future } @@ -407,7 +411,7 @@ object WorldSession { .DropItem(localItem, localPos.getOrElse(localContainer.Position), Vector3.z(localContainer.Orientation.z)), localContainer.Actor ) - case _ => ; + case _ => () } result } @@ -584,7 +588,7 @@ object WorldSession { case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //swapItem is not registered right now, we can not drop the item without re-registering it TaskWorkflow.execute(PutNewEquipmentInInventorySlot(localSource)(swapItem, localSrcSlot)) - case _ => ; + case _ => () } override def description(): String = s"unregistering $localItem before stowing in $localDestination" @@ -597,7 +601,7 @@ object WorldSession { localChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, guid) ) - case None => ; + case None => () } val moveResult = ask(localDestination.Actor, Containable.PutItemInSlotOrAway(localItem, Some(localDestSlot))) moveResult.onComplete(localMoveOnComplete) @@ -610,7 +614,7 @@ object WorldSession { moveItemTaskFunc(fromSlot), GUIDTask.unregisterEquipment(fromSource.Zone.GUID, itemToMove) )) - case _ => ; + case _ => () } val result = ask(source.Actor, Containable.RemoveItemFromSlot(item)) result.onComplete(resultOnComplete) @@ -689,7 +693,7 @@ object WorldSession { case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //swapItem is not registered right now, we can not drop the item without re-registering it TaskWorkflow.execute(PutNewEquipmentInInventorySlot(localSource)(swapItem, localSrcSlot)) - case _ => ; + case _ => () } override def description(): String = s"registering $localItem in ${localDestination.Zone.id} before removing from $localSource" @@ -702,7 +706,7 @@ object WorldSession { localChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, guid) ) - case None => ; + case None => () } val moveResult = ask(localDestination.Actor, Containable.PutItemInSlotOrAway(localItem, Some(localDestSlot))) moveResult.onComplete(localMoveOnComplete) @@ -715,7 +719,7 @@ object WorldSession { moveItemTaskFunc(fromSlot), GUIDTask.registerEquipment(fromSource.Zone.GUID, itemToMove) )) - case _ => ; + case _ => () } val result = ask(source.Actor, Containable.RemoveItemFromSlot(item)) result.onComplete(resultOnComplete) @@ -847,17 +851,17 @@ object WorldSession { case Some(e) => log.info(s"${tplayer.Name} has dropped ${tplayer.Sex.possessive} ${e.Definition.Name}") PutEquipmentInInventoryOrDrop(tplayer)(e) - case _ => ; + case _ => () } //restore previously-held-up equipment itemInPreviouslyDrawnSlotToDrop match { case Some(e) => PutEquipmentInInventorySlot(tplayer)(e, previouslyDrawnSlot) - case _ => ; + case _ => () } log.info(s"${tplayer.Name} has quickly drawn a ${grenade.Definition.Name}") - case _ => ; + case _ => () } - case None => ; + case None => () } optGrenadeInSlot.nonEmpty } else { diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index 086d7d04d..6a66afe33 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -10,8 +10,8 @@ import net.psforever.objects.serverobject.transfer.TransferContainer import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.vehicles._ import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, TriggeredSound} -import net.psforever.types.{DriveState, PlanetSideEmpire, PlanetSideGUID, Vector3} +import net.psforever.packet.game.{ChatMsg, HackMessage, HackState, HackState1, HackState7, TriggeredSound} +import net.psforever.types.{ChatMessageType, DriveState, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -46,7 +46,7 @@ object Vehicles { vehicle.Zone.id, VehicleAction.Ownership(tplayer.GUID, vehicle.GUID) ) - Vehicles.ReloadAccessPermissions(vehicle, tplayer.Name) + Vehicles.ReloadAccessPermissions(vehicle, tplayer.Faction.toString) Some(vehicle) case None => None @@ -61,31 +61,37 @@ object Vehicles { * @return the vehicle, if it had a previous owner; * `None`, otherwise */ - def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] = - vehicle.Zone.GUID(vehicle.OwnerGuid) match { - case Some(player: Player) => - if (player.avatar.vehicle.contains(guid)) { - player.avatar.vehicle = None -// vehicle.Zone.VehicleEvents ! VehicleServiceMessage( -// player.Name, -// VehicleAction.Ownership(player.GUID, PlanetSideGUID(0)) -// ) - } - vehicle.AssignOwnership(None) - val empire = VehicleLockState.Empire.id - //val factionChannel = s"${vehicle.Faction}" - (0 to 2).foreach(group => { - vehicle.PermissionGroup(group, empire) - /*vehicle.Zone.VehicleEvents ! VehicleServiceMessage( - factionChannel, - VehicleAction.SeatPermissions(Service.defaultPlayerGUID, guid, group, empire) - )*/ - }) - ReloadAccessPermissions(vehicle, player.Name) - Some(vehicle) - case _ => - None - } + def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] = { + val zone = vehicle.Zone + val ownerGuid = vehicle.OwnerGuid + val ownerName = vehicle.OwnerName + vehicle.AssignOwnership(None) + val result = zone + .GUID(ownerGuid) + .collect { + case player: Player => player.avatar + } + .orElse { + zone + .Players + .collectFirst { + case avatar if ownerName.contains(avatar.name) => avatar + } + } + .collect { + case avatar if avatar.vehicle.contains(guid) => + avatar.vehicle = None + vehicle + } + val empire = VehicleLockState.Empire.id + (0 to 2).foreach(group => vehicle.PermissionGroup(group, empire)) + ReloadAccessPermissions(vehicle, vehicle.Faction.toString) + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.LoseOwnership(ownerGuid.getOrElse(Service.defaultPlayerGUID), guid) + ) + result + } /** * Disassociate a player from a vehicle that he owns. @@ -140,7 +146,7 @@ object Vehicles { VehicleAction.SeatPermissions(pguid, vguid, group, empire) )*/ }) - ReloadAccessPermissions(vehicle, player.Name) + ReloadAccessPermissions(vehicle, vehicle.Faction.toString) Some(vehicle) } else { None @@ -239,6 +245,8 @@ object Vehicles { val zone = target.Zone val zoneid = zone.id val vehicleEvents = zone.VehicleEvents + val localEvents = zone.LocalEvents + val previousOwnerName = target.OwnerName.getOrElse("") vehicleEvents ! VehicleServiceMessage( zoneid, VehicleAction.SendResponse( @@ -292,10 +300,20 @@ object Vehicles { AvatarAction.SetEmpire(Service.defaultPlayerGUID, tGuid, hFaction) ) } - zone.LocalEvents ! LocalServiceMessage( + localEvents ! LocalServiceMessage( zoneid, LocalAction.TriggerSound(hGuid, TriggeredSound.HackVehicle, target.Position, 30, 0.49803925f) ) + if (zone.Players.exists(_.name.equals(previousOwnerName))) { + localEvents ! LocalServiceMessage( + previousOwnerName, + LocalAction.SendResponse(ChatMsg(ChatMessageType.UNK_226, "@JackStolen")) + ) + } + localEvents ! LocalServiceMessage( + hacker.Name, + LocalAction.SendResponse(ChatMsg(ChatMessageType.UNK_226, "@JackVehicleOwned")) + ) // Clean up after specific vehicles, e.g. remove router telepads // If AMS is deployed, swap it to the new faction target.Definition match { diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 1d0746b5d..2ef4fa023 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -522,6 +522,12 @@ 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") + if (reason.startsWith("@")) { + player.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.SendResponse(Service.defaultPlayerGUID, ChatMsg(ChatMessageType.UNK_227, reason)) + ) + } case Player.BuildDeployable(obj: TelepadDeployable, tool: Telepad) => obj.Router = tool.Router //necessary; forwards link to the router that produced the telepad @@ -631,6 +637,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm if (player.DrawnSlot != Player.HandsDownSlot) { player.DrawnSlot = Player.HandsDownSlot } + val dropPred = ContainableBehavior.DropPredicate(player) val (toDelete, toDrop, afterHolsters, afterInventory) = if (originalSuit == ExoSuitType.MAX) { //was max val (delete, insert) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) @@ -641,18 +648,21 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //changing to a vanilla exo-suit val (newHolsters, unplacedHolsters) = Players.fillEmptyHolsters(player.Holsters().iterator, insert ++ beforeInventory) val (inventory, unplacedInventory) = GridInventory.recoverInventory(unplacedHolsters, player.Inventory) - (delete, unplacedInventory.map(InventoryItem(_, -1)), newHolsters, inventory) + val (dropFromUnplaced, deleteFromUnplaced) = unplacedInventory.map(InventoryItem(_, -1)).partition(dropPred) + (delete ++ deleteFromUnplaced, dropFromUnplaced, newHolsters, inventory) } } else if (willBecomeMax) { //will be max, drop everything but melee slot val (melee, other) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Melee) val (inventory, unplacedInventory) = GridInventory.recoverInventory(beforeInventory ++ other, player.Inventory) - (Nil, unplacedInventory.map(InventoryItem(_, -1)), melee, inventory) + val (dropFromUnplaced, deleteFromUnplaced) = unplacedInventory.map(InventoryItem(_, -1)).partition(dropPred) + (deleteFromUnplaced, dropFromUnplaced, melee, inventory) } else { //was not a max nor will become a max; vanilla exo-suit to a vanilla-exo-suit val (insert, unplacedHolsters) = Players.fillEmptyHolsters(player.Holsters().iterator, beforeHolsters ++ beforeInventory) val (inventory, unplacedInventory) = GridInventory.recoverInventory(unplacedHolsters, player.Inventory) - (Nil, unplacedInventory.map(InventoryItem(_, -1)), insert, inventory) + val (dropFromUnplaced, deleteFromUnplaced) = unplacedInventory.map(InventoryItem(_, -1)).partition(dropPred) + (deleteFromUnplaced, dropFromUnplaced, insert, inventory) } //insert afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj) diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithGantry.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithGantry.scala index 5f637e003..35f084055 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/WithGantry.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithGantry.scala @@ -2,14 +2,15 @@ package net.psforever.objects.avatar.interaction import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} -import net.psforever.objects.{Vehicle, Vehicles} +import net.psforever.objects.{Player, Vehicle, Vehicles} import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait, GantryDenialField, PieceOfEnvironment, interaction} import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.zones.InteractsWithZone -import net.psforever.packet.game.{PlayerStateShiftMessage, ShiftState} +import net.psforever.packet.game.{ChatMsg, PlayerStateShiftMessage, ShiftState} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.hart.ShuttleState +import net.psforever.types.ChatMessageType import scala.annotation.unused import scala.concurrent.duration._ @@ -28,17 +29,26 @@ class WithGantry(val channel: String) (zone.GUID(field.obbasemesh) match { case Some(pad : OrbitalShuttlePad) => zone.GUID(pad.shuttle) case _ => None - }) match { - case Some(shuttle: Vehicle) - if shuttle.Flying.contains(ShuttleState.State11.id) || shuttle.Faction != obj.Faction => + }, obj) match { + case (Some(shuttle: Vehicle), player: Player) + if (shuttle.Flying.contains(ShuttleState.State11.id) || shuttle.Faction != player.Faction) && + player.VehicleSeated.isEmpty => val (pos, ang) = Vehicles.dismountShuttle(shuttle, field.mountPoint) - shuttle.Zone.AvatarEvents ! AvatarServiceMessage( + val events = shuttle.Zone.AvatarEvents + events ! AvatarServiceMessage( channel, AvatarAction.SendResponse( Service.defaultPlayerGUID, PlayerStateShiftMessage(ShiftState(0, pos, ang, None))) ) - case Some(_: Vehicle) => + events ! AvatarServiceMessage( + channel, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + ChatMsg(ChatMessageType.UNK_227, "@Vehicle_OS_PlacedOutsideHallway") + ) + ) + case (Some(_: Vehicle) , _)=> obj.Actor ! RespondsToZoneEnvironment.Timer( attribute, delay = 250 milliseconds, diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala index 333ae6cb1..674b6dbfb 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala @@ -1,11 +1,11 @@ // Copyright (c) 2024 PSForever package net.psforever.objects.avatar.interaction -import net.psforever.objects.Player +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player} import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.environment.interaction.common.Watery import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget -import net.psforever.objects.serverobject.environment.{PieceOfEnvironment, interaction} +import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment, interaction} import net.psforever.objects.zones.InteractsWithZone import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.OxygenState @@ -15,9 +15,11 @@ import scala.concurrent.duration._ class WithWater(val channel: String) extends InteractionWith with Watery { + /** do this every time we're in sufficient contact with water */ + private var doInteractingWithBehavior: (InteractsWithZone, PieceOfEnvironment, Option[Any]) => Unit = wadingBeforeDrowning + /** - * Water causes players to slowly suffocate. - * When they (finally) drown, they will die. + * Water is wet. * @param obj the target * @param body the environment */ @@ -26,42 +28,110 @@ class WithWater(val channel: String) body: PieceOfEnvironment, data: Option[Any] ): Unit = { - val extra = data.collect { - case t: OxygenStateTarget => Some(t) - case w: Watery => w.Condition - }.flatten - if (extra.isDefined) { - //inform the player that their mounted vehicle is in trouble (that they are in trouble (but not from drowning (yet))) - stopInteractingWith(obj, body, data) + if (getExtra(data).nonEmpty) { + inheritAndPushExtraData(obj, body, data) } else { - val (effect, time, percentage) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) - if (effect) { - val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage) - waterInteractionTime = System.currentTimeMillis() + time - condition = Some(cond) - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, Player.Die()) - //inform the player that they are in trouble - obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) - } + depth = math.max(0f, body.collision.altitude - obj.Position.z) + doInteractingWithBehavior(obj, body, data) + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 500 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("wading"))) } } /** - * When out of water, the player is no longer suffocating. - * The player does have to endure a recovery period to get back to normal, though. + * Wading only happens while the player's head is above the water. * @param obj the target * @param body the environment */ - override def stopInteractingWith( + private def wadingBeforeDrowning( obj: InteractsWithZone, body: PieceOfEnvironment, data: Option[Any] ): Unit = { - val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) - if (percentage > 99f) { - recoverFromInteracting(obj) + //we're already "wading", let's see if we're drowning + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //drowning + beginDrowning(obj, body, data) } else { - stopInteractingAction(obj, body, data, effect, time, percentage) + //inform the player that their mounted vehicle is in trouble (that they are in trouble (but not from drowning (yet))) + val extra = getExtra(data) + if (extra.nonEmpty) { + displayOxygenState( + obj, + condition.getOrElse(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, 95f)), + extra + ) + } + } + } + + /** + * Too much water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + */ + private def beginDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val (effect, time, percentage) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) + if (effect) { + val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage) + waterInteractionTime = System.currentTimeMillis() + time + condition = Some(cond) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, Player.Die()) + //inform the player that they are in trouble + displayOxygenState(obj, cond, getExtra(data)) + doInteractingWithBehavior = drowning + } + } + + /** + * Too much water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + */ + private def drowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + //test if player ever gets head above the water level + if (depth < GlobalDefinitions.MaxDepth(obj)) { + val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) + //switch to recovery + if (percentage > 0) { + recoverFromDrowning(obj, body, data) + doInteractingWithBehavior = recoverFromDrowning + } + } + } + + /** + * When out of water, the player is no longer suffocating. + * The player does have to endure a recovery period to get back to normal, though. + * @param obj the target + * @param body the environment + */ + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val state = condition.map(_.state) + if (state.contains(OxygenState.Suffocation)) { + //set up for recovery + val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 99f) { + //we're not too far gone + recoverFromDrowning(obj, body, data, effect, time, percentage) + } + doInteractingWithBehavior = recovering + } else { + doInteractingWithBehavior = wadingBeforeDrowning } } @@ -74,38 +144,169 @@ class WithWater(val channel: String) * @param time current time until completion of the next effect * @param percentage value to display in the drowning UI progress bar */ - private def stopInteractingAction( - obj: InteractsWithZone, - body: PieceOfEnvironment, - data: Option[Any], - effect: Boolean, - time: Long, - percentage: Float - ): Unit = { + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any], + effect: Boolean, + time: Long, + percentage: Float + ): Unit = { val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage) val extra = data.collect { case t: OxygenStateTarget => Some(t) case w: Watery => w.Condition }.flatten - if (effect) { + if (effect) { condition = Some(cond) waterInteractionTime = System.currentTimeMillis() + time - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) //inform the player - obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + displayOxygenState(obj, cond, extra) } else if (extra.isDefined) { //inform the player - obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + displayOxygenState(obj, cond, extra) + } + } + + /** + * The recovery period is much faster than the drowning process. + * Check for when the player fully recovers, + * and that the player does not regress back to drowning. + * @param obj the target + * @param body the environment + */ + def recovering( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + lazy val state = condition.map(_.state) + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //go back to drowning + beginDrowning(obj, body, data) + } else if (state.contains(OxygenState.Recovery)) { + //check recovery conditions + val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 1f) { + doInteractingWithBehavior = wadingBeforeDrowning + } + } + } + + /** + * When out of water, the player is no longer suffocating. + * He's even stopped wading. + * The only thing we should let complete now is recovery. + * @param obj the target + * @param body the environment + */ + override def stopInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + if (getExtra(data).nonEmpty) { + inheritAndPushExtraData(obj, body, data) + } else { + stopInteractingWithAction(obj, body, data) + } + } + + /** + * When out of water, the player is no longer suffocating. + * He's even stopped wading. + * The only thing we should let complete now is recovery. + * @param obj the target + * @param body the environment + */ + private def stopInteractingWithAction( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val cond = condition.map(_.state) + if (cond.contains(OxygenState.Suffocation)) { + //go from suffocating to recovery + recoverFromDrowning(obj, body, data) + } else if (cond.isEmpty) { + //neither suffocating nor recovering, so just reset everything + recoverFromInteracting(obj) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(attribute) + waterInteractionTime = 0L + depth = 0f + condition = None + doInteractingWithBehavior = wadingBeforeDrowning } } override def recoverFromInteracting(obj: InteractsWithZone): Unit = { super.recoverFromInteracting(obj) - if (condition.exists(_.state == OxygenState.Suffocation)) { - val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) - stopInteractingAction(obj, condition.map(_.body).get, None, effect, time, percentage) + val cond = condition.map(_.state) + //whether or not we were suffocating or recovering, we need to undo the visuals for that + if (cond.nonEmpty) { + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + displayOxygenState( + obj, + OxygenStateTarget(obj.GUID, condition.map(_.body).get, OxygenState.Recovery, 100f), + None + ) } - waterInteractionTime = 0L condition = None } + + /** + * From the "condition" of someone else's drowning status, + * extract target information and progress. + * @param data any information + * @return target information and drowning progress + */ + private def getExtra(data: Option[Any]): Option[OxygenStateTarget] = { + data.collect { + case t: OxygenStateTarget => Some(t) + case w: Watery => w.Condition + }.flatten + } + + /** + * Send the message regarding drowning and recovery + * that includes additional information about a related target that is drowning or recovering. + * @param obj the target + * @param body the environment + * @param data essential information about someone else's interaction with water + */ + private def inheritAndPushExtraData( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val state = condition.map(_.state).getOrElse(OxygenState.Recovery) + val Some((_, _, percentage)) = state match { + case OxygenState.Suffocation => Some(Watery.drowningInWateryConditions(obj, Some(state), waterInteractionTime)) + case OxygenState.Recovery => Some(Watery.recoveringFromWateryConditions(obj, Some(state), waterInteractionTime)) + } + displayOxygenState(obj, OxygenStateTarget(obj.GUID, body, state, percentage), getExtra(data)) + } + + /** + * Send the message regarding drowning and recovery. + * @param obj the target + * @param cond the environment + */ + private def displayOxygenState( + obj: InteractsWithZone, + cond: OxygenStateTarget, + data: Option[OxygenStateTarget] + ): Unit = { + obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, data)) + } +} + +object WithWater { + /** special environmental trait to queue actions independent from the primary wading test */ + case object WaterAction extends EnvironmentTrait { + override def canInteractWith(obj: PlanetSideGameObject): Boolean = false + override def testingDepth(obj: PlanetSideGameObject): Float = Float.PositiveInfinity + } } 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 b7b430737..8791b6397 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala @@ -9,15 +9,15 @@ import net.psforever.types.PlanetSideGUID import scala.util.{Failure, Success, Try} class SmallDeployableConverter extends ObjectCreateConverter[Deployable]() { - override def ConstructorData(obj: Deployable): Try[CommonFieldDataWithPlacement] = { + override def ConstructorData(obj: Deployable): Try[SmallDeployableData] = { Success( - CommonFieldDataWithPlacement( + SmallDeployableData(CommonFieldDataWithPlacement( PlacementData(obj.Position, obj.Orientation), CommonFieldData( obj.Faction, bops = false, alternate = obj.Destroyed, - false, + v1 = false, None, jammered = obj match { case o: JammableUnit => o.Jammed @@ -30,10 +30,10 @@ class SmallDeployableConverter extends ObjectCreateConverter[Deployable]() { case None => PlanetSideGUID(0) } ) - ) + )) ) } - override def DetailedConstructorData(obj: Deployable): Try[CommonFieldDataWithPlacement] = + override def DetailedConstructorData(obj: Deployable): Try[SmallDeployableData] = Failure(new Exception("converter should not be used to generate detailed small deployable data")) } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index 11059fbab..3c4650132 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -801,7 +801,7 @@ object GlobalDefinitionsMiscellaneous { AutoRanges( detection = 125f, trigger = 100f, - escape = 200f + escape = 150f ), AutoChecks( validation = List( @@ -813,7 +813,7 @@ object GlobalDefinitionsMiscellaneous { ), retaliatoryDelay = 4000L, //8000L cylindrical = true, - cylindricalExtraHeight = 50f, + cylindricalExtraHeight = 25f, detectionSweepTime = 2.seconds, refireTime = 362.milliseconds //312.milliseconds ) diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala index c3dd0c9dd..54b540648 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala @@ -971,7 +971,7 @@ object GlobalDefinitionsVehicle { ams.DeployTime = 2000 ams.UndeployTime = 2000 ams.interference = InterferenceRange(main = 125f, sharedGroupId = 3, shared = 30f) - ams.DeconstructionTime = Some(20 minutes) + ams.DeconstructionTime = Some(15 minutes) ams.AutoPilotSpeeds = (18, 6) ams.Packet = utilityConverter ams.DestroyedModel = Some(DestroyedVehicle.Ams) @@ -1014,6 +1014,7 @@ object GlobalDefinitionsVehicle { router.Deployment = true router.DeployTime = 2000 router.UndeployTime = 2000 + router.interference = InterferenceRange(main = 20f) router.DeconstructionTime = Duration(20, "minutes") router.AutoPilotSpeeds = (16, 6) router.Packet = variantConverter diff --git a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala index 76be594b3..d3680de99 100644 --- a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala @@ -676,6 +676,7 @@ object ContainableBehavior { entry => { val objDef = entry.obj.Definition val faction = GlobalDefinitions.isFactionEquipment(objDef) + GlobalDefinitions.isCavernEquipment(objDef) || objDef == GlobalDefinitions.router_telepad || entry.obj.isInstanceOf[BoomerTrigger] || (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) diff --git a/src/main/scala/net/psforever/objects/serverobject/deploy/DeploymentBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/deploy/DeploymentBehavior.scala index 664facf54..0eb8cf718 100644 --- a/src/main/scala/net/psforever/objects/serverobject/deploy/DeploymentBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/deploy/DeploymentBehavior.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.deploy -import akka.actor.{Actor, Cancellable} +import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.objects.Default import net.psforever.types.{DriveState, Vector3} import net.psforever.services.Service @@ -33,24 +33,24 @@ trait DeploymentBehavior { val deployBehavior: Receive = { case Deployment.TryDeploymentChange(state) => - sender() ! TryDeploymentStateChange(state) + sender() ! TryDeploymentStateChange(state, sender()) case Deployment.TryDeploy(state) => - sender() ! TryDeployStateChange(state) + sender() ! TryDeployStateChange(state, sender()) case Deployment.TryUndeploy(state) => - sender() ! TryUndeployStateChange(state) + sender() ! TryUndeployStateChange(state, sender()) } - def TryDeploymentStateChange(state: DriveState.Value): Any = { + def TryDeploymentStateChange(state: DriveState.Value, replyTo: ActorRef): Any = { val obj = DeploymentObject val prevState = obj.DeploymentState if (TryDeploymentChange(obj, state)) { if (Deployment.CheckForDeployState(state)) { - DeploymentAction(obj, state, prevState) + DeploymentAction(obj, state, prevState, replyTo) Deployment.CanDeploy(obj, state) } else { - UndeploymentAction(obj, state, prevState) + UndeploymentAction(obj, state, prevState, replyTo) Deployment.CanUndeploy(obj, state) } } else { @@ -58,22 +58,22 @@ trait DeploymentBehavior { } } - def TryDeployStateChange(state: DriveState.Value): Any = { + def TryDeployStateChange(state: DriveState.Value, replyTo: ActorRef): Any = { val obj = DeploymentObject val prevState = obj.DeploymentState if (Deployment.CheckForDeployState(state) && TryDeploymentChange(obj, state)) { - DeploymentAction(obj, state, prevState) + DeploymentAction(obj, state, prevState, replyTo) Deployment.CanDeploy(obj, state) } else { Deployment.CanNotChangeDeployment(obj, state, "incorrect deploy transition state") } } - def TryUndeployStateChange(state: DriveState.Value): Any = { + def TryUndeployStateChange(state: DriveState.Value, replyTo: ActorRef): Any = { val obj = DeploymentObject val prevState = obj.DeploymentState if (Deployment.CheckForUndeployState(state) && TryUndeploymentChange(obj, state)) { - UndeploymentAction(obj, state, prevState) + UndeploymentAction(obj, state, prevState, replyTo) Deployment.CanUndeploy(obj, state) } else { Deployment.CanNotChangeDeployment(obj, state, "incorrect undeploy transition state") @@ -91,7 +91,8 @@ trait DeploymentBehavior { def DeploymentAction( obj: Deployment.DeploymentObject, state: DriveState.Value, - prevState: DriveState.Value + prevState: DriveState.Value, + replyTo: ActorRef ): DriveState.Value = { val guid = obj.GUID val zone = obj.Zone @@ -108,11 +109,9 @@ trait DeploymentBehavior { VehicleAction.DeployRequest(GUID0, guid, state, 0, unk2=false, Vector3.Zero) ) deploymentTimer.cancel() - deploymentTimer = context.system.scheduler.scheduleOnce( - obj.DeployTime milliseconds, - obj.Actor, - Deployment.TryDeploy(DriveState.Deployed) - ) + deploymentTimer = context.system.scheduler.scheduleOnce(obj.DeployTime.milliseconds)({ + obj.Actor.tell(Deployment.TryDeploy(DriveState.Deployed), replyTo) + }) state } else if (state == DriveState.Deployed) { obj.Velocity = Some(Vector3.Zero) //no velocity @@ -129,7 +128,8 @@ trait DeploymentBehavior { def UndeploymentAction( obj: Deployment.DeploymentObject, state: DriveState.Value, - prevState: DriveState.Value + prevState: DriveState.Value, + replyTo: ActorRef ): DriveState.Value = { val guid = obj.GUID val zone = obj.Zone @@ -142,11 +142,9 @@ trait DeploymentBehavior { ) import scala.concurrent.ExecutionContext.Implicits.global deploymentTimer.cancel() - deploymentTimer = context.system.scheduler.scheduleOnce( - obj.UndeployTime milliseconds, - obj.Actor, - Deployment.TryUndeploy(DriveState.Mobile) - ) + deploymentTimer = context.system.scheduler.scheduleOnce(obj.UndeployTime.milliseconds)({ + obj.Actor.tell(Deployment.TryUndeploy(DriveState.Mobile), replyTo) + }) state } else if (state == DriveState.Mobile) { zone.VehicleEvents ! VehicleServiceMessage( diff --git a/src/main/scala/net/psforever/objects/serverobject/deploy/Interference.scala b/src/main/scala/net/psforever/objects/serverobject/deploy/Interference.scala index 8b0cae70e..da5fba1a4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/deploy/Interference.scala +++ b/src/main/scala/net/psforever/objects/serverobject/deploy/Interference.scala @@ -46,11 +46,11 @@ object Interference { * @param zone game world in which this test will be conducted; * entity should be `ZoneAware`, but it may not be set correctly during this part of its internal process * @param obj entity that may be interfered with - * @return a different entity that causes the test entity to suffer interference + * @return other entities that causes the test entity to suffer interference */ - def Test(zone: Zone, obj: PlanetSideGameObject with FactionAffinity): Option[PlanetSideGameObject with FactionAffinity] = { + def Test(zone: Zone, obj: PlanetSideGameObject with FactionAffinity): Seq[PlanetSideGameObject with FactionAffinity] = { val (data, filterFunc) = SetupForTest(zone, obj) - data.find(filterFunc) + data.filter(filterFunc) } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala index 2ad7d1e87..232923956 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala @@ -10,6 +10,8 @@ import net.psforever.types.Vector3 */ abstract class EnvironmentTrait { def canInteractWith(obj: PlanetSideGameObject): Boolean + + def testingDepth(obj: PlanetSideGameObject): Float } object EnvironmentAttribute { @@ -25,16 +27,33 @@ object EnvironmentAttribute { case _ => false }) } + + def testingDepth(obj: PlanetSideGameObject): Float = { + obj match { + case v: Vehicle if v.Flying.nonEmpty => + 0f + case _: Vehicle if !obj.Definition.DrownAtMaxDepth => + obj.Definition.MaxDepth * 0.9f + case _: Vehicle => + obj.Definition.MaxDepth * 0.6f + case _ => + 0.2f + } + } } case object Lava extends EnvironmentTrait { /** lava can only interact with anything capable of registering damage */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj) + + def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 0f } case object Death extends EnvironmentTrait { /** death can only interact with anything capable of registering damage */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj) + + def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 0f } case object GantryDenialField @@ -46,18 +65,24 @@ object EnvironmentAttribute { case _ => false } } + + def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 0f } case object MovementFieldTrigger extends EnvironmentTrait { /** only interact with living player characters or vehicles */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) + + def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 0f } case object InteriorField extends EnvironmentTrait { /** only interact with living player characters or vehicles */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) + + def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 0f } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala index aacd261b9..33c550209 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala @@ -1,7 +1,6 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.serverobject.environment.interaction -import net.psforever.objects.GlobalDefinitions import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment} import net.psforever.objects.zones._ @@ -98,6 +97,8 @@ class InteractWithEnvironment() .foreach(_.stopInteractingWith(obj, body, None)) } } + + def OngoingInteractions: Set[EnvironmentTrait] = interactWith.map(_.attribute) } object InteractWithEnvironment { @@ -117,9 +118,8 @@ object InteractWithEnvironment { obj: PlanetSideServerObject, sector: SectorPopulation ): Set[PieceOfEnvironment] = { - val depth = GlobalDefinitions.MaxDepth(obj) sector.environmentList - .filter(body => body.attribute.canInteractWith(obj) && body.testInteraction(obj, depth)) + .filter(body => body.attribute.canInteractWith(obj) && body.testInteraction(obj, body.attribute.testingDepth(obj))) .distinctBy(_.attribute) .toSet } @@ -136,7 +136,7 @@ object InteractWithEnvironment { body: PieceOfEnvironment, obj: PlanetSideServerObject ): Option[PieceOfEnvironment] = { - if ((obj.Zone eq zone) && body.testInteraction(obj, GlobalDefinitions.MaxDepth(obj))) { + if ((obj.Zone eq zone) && body.testInteraction(obj, body.attribute.testingDepth(obj))) { Some(body) } else { None @@ -185,12 +185,12 @@ case class OnStableEnvironment() extends InteractionBehavior { ): Set[PieceOfEnvironment] = { if (allow) { val interactions = obj.interaction().collectFirst { case inter: InteractWithEnvironment => inter.Interactions } - val env = InteractWithEnvironment.checkAllEnvironmentInteractions(obj, sector) - env.foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.doInteractingWith(obj, body, None))) - if (env.nonEmpty) { + val bodies = InteractWithEnvironment.checkAllEnvironmentInteractions(obj, sector) + bodies.foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.doInteractingWith(obj, body, None))) + if (bodies.nonEmpty) { nextstep = AwaitOngoingInteraction(obj.Zone) } - env + bodies } else { nextstep = BlockedFromInteracting() Set() @@ -225,17 +225,22 @@ final case class AwaitOngoingInteraction(zone: Zone) extends InteractionBehavior ): Set[PieceOfEnvironment] = { val interactions = obj.interaction().collectFirst { case inter: InteractWithEnvironment => inter.Interactions } if (allow) { - val env = InteractWithEnvironment.checkAllEnvironmentInteractions(obj, sector) + val bodies = InteractWithEnvironment.checkAllEnvironmentInteractions(obj, sector) val (in, out) = existing.partition(body => InteractWithEnvironment.checkSpecificEnvironmentInteraction(zone, body, obj).nonEmpty) - env.diff(in).foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.doInteractingWith(obj, body, None))) - out.foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.stopInteractingWith(obj, body, None))) - if (env.isEmpty) { + val inAttrs = bodies.map(_.attribute) + out + .filterNot(e => inAttrs.contains(e.attribute)) + .foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.stopInteractingWith(obj, body, None))) + bodies + .diff(in) + .foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.doInteractingWith(obj, body, None))) + if (bodies.isEmpty) { val n = OnStableEnvironment() val out = n.perform(obj, sector, Set(), allow) nextstep = n.next out } else { - env + bodies } } else { existing.foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.stopInteractingWith(obj, body, None))) diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala index 799c166c8..e08cd8b27 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala @@ -1,20 +1,26 @@ // Copyright (c) 2024 PSForever package net.psforever.objects.serverobject.environment.interaction.common +import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.environment.interaction.InteractWithEnvironment import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait, PieceOfEnvironment} +import net.psforever.objects.zones.InteractsWithZone import net.psforever.types.{OxygenState, PlanetSideGUID} trait Watery { val attribute: EnvironmentTrait = EnvironmentAttribute.Water - /** how long the current interaction has been progressing in the current way */ protected var waterInteractionTime: Long = 0 /** information regarding the drowning state */ protected var condition: Option[OxygenStateTarget] = None /** information regarding the drowning state */ def Condition: Option[OxygenStateTarget] = condition + /** how far the player's feet are below the surface of the water */ + protected var depth: Float = 0f + /** how far the player's feet are below the surface of the water */ + def Depth: Float = depth } object Watery { @@ -33,6 +39,27 @@ object Watery { progress: Float ) + /** + * na + * @param target evaluate this to determine if to continue with this loss + * @return whether or not we are sufficiently submerged in water + */ + def wading(target: PlanetSideGameObject with InteractsWithZone): Boolean = { + target + .interaction() + .collectFirst { + case env: InteractWithEnvironment => + env + .Interactions + .get(EnvironmentAttribute.Water) + .collectFirst { + case water: Watery => water.Depth > 0f + } + } + .flatten + .contains(true) + } + /** * Calculate the effect of being exposed to a watery environment beyond an entity's critical region. * @param obj the target @@ -122,10 +149,10 @@ object Watery { //switching from suffocation to recovery val oldDuration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Suffocation) val newDuration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Recovery) - val oldTimeRemaining: Long = completionTime - System.currentTimeMillis() + val oldTimeRemaining: Long = math.max(0, completionTime - System.currentTimeMillis()) val oldTimeRatio: Float = oldTimeRemaining / oldDuration.toFloat val percentage: Float = oldTimeRatio * 100 - val recoveryTime: Long = newDuration - (newDuration * oldTimeRatio).toLong + val recoveryTime: Long = newDuration * (1f - oldTimeRatio).toLong (true, recoveryTime, percentage) case Some(OxygenState.Recovery) => //interrupted while recovering, calculate the progress and keep recovering diff --git a/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala b/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala index d13627e9f..bb02a6970 100644 --- a/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala +++ b/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala @@ -34,6 +34,7 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity { private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var carrier: Option[Player] = None private var lastTimeCollected: Long = System.currentTimeMillis() + private val spawnedTime: Long = lastTimeCollected def Target: Building = target def Target_=(newTarget: Building): Building = { @@ -56,10 +57,11 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity { * When the flag is carried by a player, the position returned should be that of the carrier not the flag. * @return the position of the carrier, if there is a player carrying the flag, or the flag itself */ - override def Position: Vector3 = if (Carrier.nonEmpty) { - carrier.get.Position - } else { - super.Position + override def Position: Vector3 = { + carrier match { + case Some(player) => player.Position + case None => super.Position + } } def Carrier: Option[Player] = carrier @@ -70,6 +72,8 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity { } def LastCollectionTime: Long = carrier.map { _ => lastTimeCollected }.getOrElse { System.currentTimeMillis() } + + def InitialSpawnTime: Long = spawnedTime } object CaptureFlag { diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala index 3d858c4d6..c797371ac 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala @@ -146,5 +146,5 @@ object Mountable { * @param obj the `Mountable` object * @param seat_num the seat index */ - final case class CanNotDismount(obj: Mountable, seat_num: Int) extends Exchange + final case class CanNotDismount(obj: Mountable, seat_num: Int, bailType: BailType.Value) extends Exchange } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala index 1c60938c6..aca66da06 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala @@ -94,7 +94,7 @@ trait MountableBehavior { ) } else { - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_number)) + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_number, bail_type)) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala index 0fa40b9e8..29bd0f511 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala @@ -56,6 +56,9 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) /** how to process either the first order or every subsequent order */ private var handleOrderFunc: VehicleSpawnPad.VehicleOrder => Unit = NewTasking + /** ... */ + private var reminderSeq: Seq[Int] = Seq() + def LogId = "" /** @@ -77,7 +80,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) } override def postStop() : Unit = { - periodicReminder.cancel() + discontinueCurrentReminder() queueManagement.cancel() } @@ -113,36 +116,11 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) case VehicleSpawnControl.ProcessControl.QueueManagement => queueManagementTask() - /* - When the vehicle is spawned and added to the pad, it will "occupy" the pad and block it from further action. - Normally, the player who wanted to spawn the vehicle will be automatically put into the driver mount. - If this is blocked, the vehicle will idle on the pad and must be moved far enough away from the point of origin. - During this time, a periodic message about the spawn pad being blocked will be broadcast to the order queue. - */ case VehicleSpawnControl.ProcessControl.Reminder => - trackedOrder - .collect { - case entry => - if (periodicReminder.isCancelled) { - trace(s"the pad has become blocked by a ${entry.vehicle.Definition.Name} in its current order") - periodicReminder = context.system.scheduler.scheduleWithFixedDelay( - VehicleSpawnControl.periodicReminderTestDelay, - VehicleSpawnControl.periodicReminderTestDelay, - self, - VehicleSpawnControl.ProcessControl.Reminder - ) - } else { - BlockedReminder(entry, orders) - } - trackedOrder - } - .orElse { - periodicReminder.cancel() - None - } + evaluateBlockedReminder() case VehicleSpawnControl.ProcessControl.Flush => - periodicReminder.cancel() + discontinueCurrentReminder() orders.foreach { CancelOrder(_, Some("@SVCP_RemovedFromVehicleQueue_Generic")) } orders = Nil trackedOrder.foreach { @@ -259,7 +237,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) * `None`, if no order found or submitted */ private def ProcessOrder(order: Option[VehicleSpawnPad.VehicleOrder]): Unit = { - periodicReminder.cancel() + discontinueCurrentReminder() order.collect { case VehicleSpawnPad.VehicleOrder(driver, vehicle, terminal) => val size = orders.size + 1 @@ -324,37 +302,6 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) } } - /** - * na - * @param blockedOrder the previous order whose vehicle is blocking the spawn pad from operating - * @param recipients all of the other customers who will be receiving the message - */ - private def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnPad.VehicleOrder]): Unit = { - val user = blockedOrder.vehicle - .Seats(0).occupant - .orElse(pad.Zone.GUID(blockedOrder.vehicle.OwnerGuid)) - .orElse(pad.Zone.GUID(blockedOrder.DriverGUID)) - val relevantRecipients: Iterator[VehicleSpawnPad.VehicleOrder] = user match { - case Some(p: Player) if !p.HasGUID => - recipients.iterator - case Some(_: Player) => - (VehicleSpawnPad.VehicleOrder( - blockedOrder.driver, - blockedOrder.vehicle, - null //permissible - ) +: recipients).iterator //one who took possession of the vehicle - case _ => - recipients.iterator - } - recursiveBlockedReminder( - relevantRecipients, - if (blockedOrder.vehicle.Health == 0) - Option("Clear the wreckage.") - else - None - ) - } - /** * Cancel this vehicle order and inform the person who made it, if possible. * @param entry the order being cancelled @@ -381,21 +328,136 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) } } + /** + * When the vehicle is spawned and added to the pad, it will "occupy" the pad and block it from further action. + * During this time, a periodic message about the spawn pad being blocked will be broadcast to the order queue.
+ * The vehicle is also queued to deconstruct in 30s if no one assumes the driver seat. + */ + private def evaluateBlockedReminder(): Unit = { + /* + Normally, the player who wanted to spawn the vehicle will be automatically put into the driver mount. + If this is blocked or aborted, the vehicle will idle on the pad and must be moved far enough away from the point of origin. + */ + trackedOrder + .collect { + case entry => + if (reminderSeq.isEmpty) { + //begin reminder + trace(s"the pad has become blocked by a ${entry.vehicle.Definition.Name} in its current order") + retimePeriodicReminder( + shaveOffFirstElementAndDiffSecondElement(pad.Definition.BlockedReminderMessageDelays) + ) + trackedOrder + } else if (reminderSeq.size == 1) { + //end reminder + standaloneBlockedReminder( + VehicleSpawnPad.VehicleOrder(entry.driver, entry.vehicle, null), + Some("@PadDeconstruct_Done") + ) + None + } else { + //continue reminder + BlockedReminder(entry, orders) + retimePeriodicReminder( + shaveOffFirstElementAndDiffSecondElement(reminderSeq) + ) + trackedOrder + } + } + .orElse { + discontinueCurrentReminder() + None + } + } + + /** + * The periodic reminder will no longer be repeated. + * Sequences tied to the periodic reminder should be reset. + */ + private def discontinueCurrentReminder(): Unit = { + periodicReminder.cancel() + periodicReminder = Default.Cancellable + reminderSeq = List() + } + + /** + * na + * @param blockedOrder the previous order whose vehicle is blocking the spawn pad from operating + * @param recipients all of the other customers who will be receiving the message + */ + private def BlockedReminder( + blockedOrder: VehicleSpawnControl.Order, + recipients: Seq[VehicleSpawnPad.VehicleOrder] + ): Unit = { + //everyone else + recursiveBlockedReminder( + recipients.iterator, + if (blockedOrder.vehicle.Health == 0) + Option("The vehicle spawn pad where you placed your order is blocked. Clearing the wreckage ...") + else + Option("The vehicle spawn pad where you placed your order is blocked.") + ) + //would-be driver + blockedOrder.vehicle + .Seats(0).occupant + .orElse(pad.Zone.GUID(blockedOrder.vehicle.OwnerGuid)) + .orElse(pad.Zone.GUID(blockedOrder.DriverGUID)) collect { + case p: Player if p.isAlive => + standaloneBlockedReminder( + VehicleSpawnPad.VehicleOrder(blockedOrder.driver, blockedOrder.vehicle, null), + Some(s"@PadDeconstruct_secsA^${reminderSeq.head}~") + ) + } + } + + /** + * Clip the first entry in a list of numbers and + * get the difference between the clipped entry and the next entry. + * The clipped-off list will be made to be the new sequence of reminder delays. + * @param sequence reminder delay test values + * @return difference between first delay and second delay + */ + private def shaveOffFirstElementAndDiffSecondElement(sequence: Seq[Int]): Int = { + val startTime = sequence.take(1).headOption.getOrElse(0) + val restTimes = sequence.drop(1) + val headOfRestTimes = restTimes.headOption.getOrElse(startTime) + reminderSeq = restTimes + startTime - headOfRestTimes + } + + /** + * Set a single instance of the "periodic reminder" to this kind of delay. + * @param delay how long until the next reminder + */ + private def retimePeriodicReminder(delay: Int): Unit = { + periodicReminder = context.system.scheduler.scheduleOnce( + delay.seconds, + self, + VehicleSpawnControl.ProcessControl.Reminder + ) + } + @tailrec private final def recursiveBlockedReminder( iter: Iterator[VehicleSpawnPad.VehicleOrder], cause: Option[Any] ): Unit = { if (iter.hasNext) { - val recipient = iter.next() - pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( - recipient.player.Name, - VehicleSpawnPad.Reminders.Blocked, - cause - ) + standaloneBlockedReminder(iter.next(), cause) recursiveBlockedReminder(iter, cause) } } + private def standaloneBlockedReminder( + entry: VehicleSpawnPad.VehicleOrder, + cause: Option[Any] + ): Unit = { + pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( + entry.player.Name, + VehicleSpawnPad.Reminders.Blocked, + cause + ) + } + @tailrec private final def recursiveOrderReminder( iter: Iterator[VehicleSpawnPad.VehicleOrder], size: Int, @@ -414,19 +476,16 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) } object VehicleSpawnControl { - private final val periodicReminderTestDelay: FiniteDuration = 10 seconds - /** * Control messages for the vehicle spawn process. */ + sealed trait ProcessControlOperation object ProcessControl { - sealed trait ProcessControl - - case object Flush extends ProcessControl - case object OrderCancelled extends ProcessControl - case object GetNewOrder extends ProcessControl - case object Reminder extends ProcessControl - case object QueueManagement extends ProcessControl + case object Flush extends ProcessControlOperation + case object OrderCancelled extends ProcessControlOperation + case object GetNewOrder extends ProcessControlOperation + case object Reminder extends ProcessControlOperation + case object QueueManagement extends ProcessControlOperation } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala index 51c4c4a3b..86b072f12 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala @@ -12,7 +12,6 @@ class VehicleSpawnPadDefinition(objectId: Int) extends AmenityDefinition(objectI // Different pads require a Z offset to stop vehicles falling through the world after the pad rises from the floor, these values are found in game_objects.adb.lst private var vehicle_creation_z_offset = 0f - // Different pads also require an orientation offset when detaching vehicles from the rails associated with the spawn pad, again in game_objects.adb.lst // For example: 9754:add_property dropship_pad_doors vehiclecreationzorientoffset 90 // However, it seems these values need to be reversed to turn CCW to CW rotation (e.g. +90 to -90) @@ -35,6 +34,12 @@ class VehicleSpawnPadDefinition(objectId: Int) extends AmenityDefinition(objectI * @see `net.psforever.objects.serverobject.pad.process.VehicleSpawnControlRailJack` */ var killBox: (VehicleSpawnPad, Boolean)=>(PlanetSideGameObject, PlanetSideGameObject, Float)=> Boolean = VehicleSpawnPadDefinition.prepareKillBox(forwardLimit = 0, backLimit = 0, sideLimit = 0, aboveLimit = 0) + + private val blockedReminderMessageDelays: Seq[Int] = Seq(30, 23, 15, 8, 0) + + def VehicleCreationDeconstructTime: Int = blockedReminderMessageDelays.head + + def BlockedReminderMessageDelays: Seq[Int] = blockedReminderMessageDelays } object VehicleSpawnPadDefinition { diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala index 516b904b2..67c8d6e61 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala @@ -50,26 +50,35 @@ class VehicleSpawnControlFinalClearance(pad: VehicleSpawnPad) extends VehicleSpa self ! VehicleSpawnControlFinalClearance.Test(order) case test @ VehicleSpawnControlFinalClearance.Test(entry) => - //the vehicle has an initial decay of 30s in which time it needs to be mounted + //the vehicle has an initial decay in which time it needs to be mounted //once mounted, it will complain to the current driver that it is blocking the spawn pad //no time limit exists for that state val vehicle = entry.vehicle - if (Vector3.DistanceSquared(vehicle.Position, pad.Position) > 144) { //12m away from pad - trace("pad cleared") - pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) - context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder - } else if (vehicle.Destroyed) { - trace("clearing the pad of vehicle wreckage") + val distanceTest = Vector3.DistanceSquared(vehicle.Position, pad.Position) > 144 //12m away from pad + if (vehicle.Destroyed) { + trace("pad cleared of vehicle wreckage") + val delay = if (distanceTest) { 2000 } else { 5000 } VehicleSpawnControl.DisposeVehicle(vehicle, pad.Zone) - context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + temp = context.system.scheduler.scheduleOnce(delay.milliseconds, self, VehicleSpawnControlFinalClearance.NextOrder) + } else if (distanceTest) { + trace("pad cleared") + val delay = if (vehicle.Seats.head._2.occupant.isEmpty) { 4500 } else { 2000 } + temp = context.system.scheduler.scheduleOnce(delay.milliseconds, self, VehicleSpawnControlFinalClearance.NextOrder) } else { - temp = context.system.scheduler.scheduleOnce(2000 milliseconds, self, test) + //retry test + temp = context.system.scheduler.scheduleOnce(delay = 2000.milliseconds, self, test) } - case _ => ; + case VehicleSpawnControlFinalClearance.NextOrder => + pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + + case _ => () } } object VehicleSpawnControlFinalClearance { - private final case class Test(entry: VehicleSpawnControl.Order) + private case class Test(entry: VehicleSpawnControl.Order) + + private case object NextOrder } diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala index bf14a8cb3..520d0c1ec 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.pad.process -import akka.actor.Props +import akka.actor.{ActorRef, Props} import net.psforever.objects.{Default, Vehicle} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} @@ -25,7 +25,7 @@ import scala.concurrent.duration._ class VehicleSpawnControlSeatDriver(pad: VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { def LogId = "-usher" - val vehicleOverride = context.actorOf( + val vehicleOverride: ActorRef = context.actorOf( Props(classOf[VehicleSpawnControlServerVehicleOverride], pad), s"${context.parent.path.name}-override" ) @@ -43,7 +43,7 @@ class VehicleSpawnControlSeatDriver(pad: VehicleSpawnPad) extends VehicleSpawnCo val driver = entry.driver val vehicle = entry.vehicle //avoid unattended vehicle blocking the pad; user should mount (and does so normally) to reset decon timer - vehicle.Actor ! Vehicle.Deconstruct(Some(30 seconds)) + vehicle.Actor ! Vehicle.Deconstruct(Some(pad.Definition.VehicleCreationDeconstructTime.seconds)) if (VehicleSpawnControl.validateOrderCredentials(pad, driver, vehicle).isEmpty) { trace("driver to be made seated in vehicle") pad.Zone.VehicleEvents ! VehicleSpawnPad.StartPlayerSeatedInVehicle(driver.Name, vehicle, pad) @@ -67,7 +67,7 @@ class VehicleSpawnControlSeatDriver(pad: VehicleSpawnPad) extends VehicleSpawnCo case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => context.parent ! msg - case _ => ; + case _ => () } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index 05a01682f..6b7b3f958 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -5,12 +5,16 @@ import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer} import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3} import net.psforever.util.Config import akka.pattern.ask import akka.util.Timeout +import net.psforever.objects.Player import net.psforever.objects.avatar.scoring.Kill +import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.zones.exp.ToDatabase +import net.psforever.packet.game.ChatMsg +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import scala.collection.mutable import scala.concurrent.duration._ @@ -31,6 +35,20 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci updateHotSpotInfoOverTime() updateTime(now) } + building.CaptureTerminal + .map(_.HackedBy) + .collect { + case Some(info@Hackable.HackInfo(_, _, start, length, _)) + if building.NtuLevel == 0 && { + val approximateHackTimeRemaining = math.max(0, start + length - System.currentTimeMillis()) + approximateHackTimeRemaining <= 300.seconds.toMillis && approximateHackTimeRemaining > 295.seconds.toMillis + } => + MajorFacilityHackParticipation.warningMessageForHackOccupiers( + building, + info, + ChatMsg(ChatMessageType.UNK_227, "@FacilityRequiresResourcesForHackCriticalWarning") + ) + } lastInfoRequest = now } @@ -287,8 +305,8 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci if (isResecured) { hackerScore } else { - val flagCarrierScore = flagCarrier.map (p => List((p.CharId, 0L, "llu"))).getOrElse(Nil) - if (playersInSoi.exists(_.CharId == hackerId) && !flagCarrierScore.exists { case (charId, _,_) => charId == hackerId }) { + val flagCarrierScore = flagCarrier.map(p => List((p.CharId, 0L, "llu"))).getOrElse(Nil) + if (playersInSoi.exists(_.CharId == hackerId) && !flagCarrierScore.exists { case (charId, _, _) => charId == hackerId }) { hackerScore ++ flagCarrierScore } else { flagCarrierScore @@ -326,3 +344,61 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci .getOrElse(list) } } + +object MajorFacilityHackParticipation { + /** + * Dispatch a message to clients affected by some change. + * Establish the hack information by referencing the capture terminal. + * @param building building entity + * @param msg message to send to affected clients + */ + def warningMessageForHackOccupiers( + building: Building, + msg: ChatMsg + ): Unit = { + building + .CaptureTerminal + .flatMap(_.HackedBy) + .foreach { hackedInfo => + warningMessageForHackOccupiers(building, hackedInfo, msg) + } + } + + /** + * Dispatch a message to clients affected by some change. + * Select individuals belonging to the hacking faction to be targets for the message. + * @param building building entity + * @param hackedInfo confirmed information about the hack state + * @param msg message to send to affected clients + */ + def warningMessageForHackOccupiers( + building: Building, + hackedInfo: Hackable.HackInfo, + msg: ChatMsg + ): Unit = { + val hackerFaction = hackedInfo.hackerFaction + warningMessageForHackOccupiers( + building, + building.PlayersInSOI.filter(_.Faction == hackerFaction), + msg + ) + } + + /** + * Dispatch a message to clients affected by some change. + * @param building building entity + * @param targets affected clients by player + * @param msg message to send to affected clients + */ + private def warningMessageForHackOccupiers( + building: Building, + targets: Iterable[Player], + msg: ChatMsg + ): Unit = { + val events = building.Zone.LocalEvents + val message = LocalAction.SendResponse(msg) + targets.foreach { player => + events ! LocalServiceMessage(player.Name, message) + } + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala index 14faf3d90..85d567390 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala @@ -1,6 +1,7 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.vehicles.control +import akka.actor.ActorRef import net.psforever.objects._ import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} @@ -58,7 +59,7 @@ class DeployingVehicleControl(vehicle: Vehicle) * Even when disabled, the vehicle can be made to undeploy. */ override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { - TryUndeployStateChange(DriveState.Undeploying) + TryUndeployStateChange(DriveState.Undeploying, self) super.PrepareForDisabled(kickPassengers) } @@ -77,9 +78,10 @@ class DeployingVehicleControl(vehicle: Vehicle) override def DeploymentAction( obj: DeploymentObject, state: DriveState.Value, - prevState: DriveState.Value + prevState: DriveState.Value, + replyTo: ActorRef ): DriveState.Value = { - val out = super.DeploymentAction(obj, state, prevState) + val out = super.DeploymentAction(obj, state, prevState, replyTo) Vehicles.ReloadAccessPermissions(vehicle, vehicle.Faction.toString) specificResponseToDeployment(state) out @@ -90,9 +92,10 @@ class DeployingVehicleControl(vehicle: Vehicle) override def UndeploymentAction( obj: DeploymentObject, state: DriveState.Value, - prevState: DriveState.Value + prevState: DriveState.Value, + replyTo: ActorRef ): DriveState.Value = { - val out = if (decaying) state else super.UndeploymentAction(obj, state, prevState) + val out = if (decaying) state else super.UndeploymentAction(obj, state, prevState, replyTo) Vehicles.ReloadAccessPermissions(vehicle, vehicle.Faction.toString) specificResponseToUndeployment(state) out diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 8a5d48a2e..66b6f2700 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -117,7 +117,8 @@ class VehicleControl(vehicle: Vehicle) case Vehicle.Ownership(Some(player)) => GainOwnership(player) - case Mountable.TryMount(user, mountPoint) if vehicle.DeploymentState == DriveState.AutoPilot => + case Mountable.TryMount(user, mountPoint) + if vehicle.DeploymentState == DriveState.AutoPilot => sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint)) case msg @ Mountable.TryMount(player, mount_point) => @@ -125,19 +126,23 @@ class VehicleControl(vehicle: Vehicle) mountCleanup(mount_point, player) // Issue 1133. Todo: There may be a better way to address the issue? - case Mountable.TryDismount(user, seat_num, _) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) && + case Mountable.TryDismount(user, seat_num, bailType) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) && (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match { case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true case _ => false }) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num)) + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - case Mountable.TryDismount(user, seat_num, _) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) && + case Mountable.TryDismount(user, seat_num, bailType) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) && (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match { case Some(entry) if System.currentTimeMillis() - entry.time < 8500L => true case _ => false }) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num)) + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.DeploymentState == DriveState.AutoPilot => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) case msg @ Mountable.TryDismount(player, seat_num, _) => dismountBehavior.apply(msg) @@ -601,9 +606,9 @@ class VehicleControl(vehicle: Vehicle) c } } - watery.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player)) + WithWater.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player)) case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Recovery) => - watery.stopInteractingWithTargets( + WithWater.stopInteractingWithTargets( player, Watery.recoveringFromWater(vehicle, watery)._3, watery.Condition.map(_.body).get, diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala index 39fd899bf..01208856a 100644 --- a/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala @@ -1,37 +1,71 @@ // Copyright (c) 2024 PSForever package net.psforever.objects.vehicles.interaction -import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Vehicle} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.environment.interaction.common.Watery import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget -import net.psforever.objects.serverobject.environment.{PieceOfEnvironment, interaction} +import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment, interaction} import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.zones.InteractsWithZone import net.psforever.types.OxygenState +import scala.annotation.unused import scala.concurrent.duration._ class WithWater() extends InteractionWith with Watery { + /** do this every time we're in sufficient contact with water */ + private var doInteractingWithBehavior: (InteractsWithZone, PieceOfEnvironment, Option[Any]) => Unit = wadingBeforeDrowning + /** - * Water causes vehicles to become disabled if they dive off too far, too deep. - * Flying vehicles do not display progress towards being waterlogged. - * They just disable outright. - * @param obj the target - * @param body the environment - * @param data additional interaction information, if applicable - */ + * Water is wet. + * @param obj the target + * @param body the environment + */ def doInteractingWith( obj: InteractsWithZone, body: PieceOfEnvironment, data: Option[Any] ): Unit = { + depth = math.max(0f, body.collision.altitude - obj.Position.z) + doInteractingWithBehavior(obj, body, data) + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 500 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("wading"))) + } + + /** + * Wading only happens while the vehicle's wheels are mostly above the water. + * @param obj the target + * @param body the environment + */ + private def wadingBeforeDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + //we're already "wading", let's see if we're drowning + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //drowning + beginDrowning(obj, body, data) + } + } + + /** + * Too much water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + */ + private def beginDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + @unused data: Option[Any] + ): Unit = { obj match { case vehicle: Vehicle => - val (effect: Boolean, time: Long, percentage: Float) = { + val (effect, time, percentage): (Boolean, Long, Float) = { val (a, b, c) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { (true, 0L, 0f) //no progress bar @@ -42,40 +76,94 @@ class WithWater() if (effect) { condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) waterInteractionTime = System.currentTimeMillis() + time - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, VehicleControl.Disable(true)) - doInteractingWithTargets( + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, VehicleControl.Disable(true)) + WithWater.doInteractingWithTargets( obj, percentage, body, vehicle.Seats.values.flatMap(_.occupants).filter(p => p.isAlive && (p.Zone eq obj.Zone)) ) + doInteractingWithBehavior = drowning } case _ => () } } /** - * When out of water, the vehicle no longer risks becoming disabled. - * It does have to endure a recovery period to get back to full dehydration - * Flying vehicles are exempt from this process due to the abrupt disability they experience. - * @param obj the target - * @param body the environment - * @param data additional interaction information, if applicable - */ - override def stopInteractingWith( - obj: InteractsWithZone, - body: PieceOfEnvironment, - data: Option[Any] - ): Unit = { + * Too much water causes vehicles to slowly disable. + * When fully waterlogged, the vehicle is completely immobile. + * @param obj the target + * @param body the environment + */ + private def drowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + //test if player ever gets head above the water level + if (depth < GlobalDefinitions.MaxDepth(obj)) { + val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) + //switch to recovery + if (percentage > 0) { + recoverFromDrowning(obj, body, data, effect, time, percentage) + doInteractingWithBehavior = recovering + } + } + } + + /** + * When out of water, the vehicle is no longer being waterlogged. + * It does have to endure a recovery period to get back to normal, though. + * @param obj the target + * @param body the environment + */ + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val state = condition.map(_.state) + if (state.contains(OxygenState.Suffocation)) { + //set up for recovery + val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 99f) { + //we're not too far gone + recoverFromDrowning(obj, body, data, effect, time, percentage) + } + doInteractingWithBehavior = recovering + } else { + doInteractingWithBehavior = wadingBeforeDrowning + } + } + + /** + * When out of water, the vehicle is no longer being waterlogged. + * It does have to endure a recovery period to get back to normal, though. + * @param obj the target + * @param body the environment + * @param effect na + * @param time current time until completion of the next effect + * @param percentage value to display in the drowning UI progress bar + */ + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + @unused data: Option[Any], + effect: Boolean, + time: Long, + percentage: Float + ): Unit = { obj match { case vehicle: Vehicle => - val (effect: Boolean, time: Long, percentage: Float) = - Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) + val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage) if (effect) { - condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) + condition = Some(cond) waterInteractionTime = System.currentTimeMillis() + time - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) - stopInteractingWithTargets( + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) + //inform the players + WithWater.stopInteractingWithTargets( obj, percentage, body, @@ -86,51 +174,108 @@ class WithWater() } } + /** + * The recovery period is much faster than the waterlogging process. + * Check for when the vehicle fully recovers, + * and that the vehicle does not regress back to waterlogging. + * @param obj the target + * @param body the environment + */ + def recovering( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + lazy val state = condition.map(_.state) + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //go back to drowning + beginDrowning(obj, body, data) + } else if (state.contains(OxygenState.Recovery)) { + //check recovery conditions + val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 1f) { + doInteractingWithBehavior = wadingBeforeDrowning + } + } + }/** + * When out of water, the vehicle no longer risks becoming disabled. + * It does have to endure a recovery period to get back to full dehydration + * Flying vehicles are exempt from this process due to the abrupt disability they experience. + * @param obj the target + * @param body the environment + */ + override def stopInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val cond = condition.map(_.state) + if (cond.contains(OxygenState.Suffocation)) { + //go from suffocating to recovery + recoverFromDrowning(obj, body, data) + } else if (cond.isEmpty) { + //neither suffocating nor recovering, so just reset everything + recoverFromInteracting(obj) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(attribute) + waterInteractionTime = 0L + depth = 0f + condition = None + doInteractingWithBehavior = wadingBeforeDrowning + } + } + override def recoverFromInteracting(obj: InteractsWithZone): Unit = { super.recoverFromInteracting(obj) if (condition.exists(_.state == OxygenState.Suffocation)) { stopInteractingWith(obj, condition.map(_.body).get, None) } - waterInteractionTime = 0L condition = None } +} + +object WithWater { + /** special environmental trait to queue actions independent from the primary wading test */ + case object WaterAction extends EnvironmentTrait { + override def canInteractWith(obj: PlanetSideGameObject): Boolean = false + override def testingDepth(obj: PlanetSideGameObject): Float = Float.PositiveInfinity + } /** - * Tell the given targets that water causes vehicles to become disabled if they dive off too far, too deep. - * @see `InteractingWithEnvironment` - * @see `OxygenState` - * @see `OxygenStateTarget` - * @param obj the target - * @param percentage the progress bar completion state - * @param body the environment - * @param targets recipients of the information - */ + * Tell the given targets that water causes vehicles to become disabled if they dive off too far, too deep. + * @see `InteractingWithEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param obj the target + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ def doInteractingWithTargets( - obj: PlanetSideServerObject, - percentage: Float, - body: PieceOfEnvironment, - targets: Iterable[PlanetSideServerObject] - ): Unit = { + obj: PlanetSideServerObject, + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) targets.foreach(_.Actor ! interaction.InteractingWithEnvironment(body, state)) } /** - * Tell the given targets that, when out of water, the vehicle no longer risks becoming disabled. - * @see `EscapeFromEnvironment` - * @see `OxygenState` - * @see `OxygenStateTarget` - * @param obj the target - * @param percentage the progress bar completion state - * @param body the environment - * @param targets recipients of the information - */ + * Tell the given targets that, when out of water, the vehicle no longer risks becoming disabled. + * @see `EscapeFromEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param obj the target + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ def stopInteractingWithTargets( - obj: PlanetSideServerObject, - percentage: Float, - body: PieceOfEnvironment, - targets: Iterable[PlanetSideServerObject] - ): Unit = { + obj: PlanetSideServerObject, + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) targets.foreach(_.Actor ! interaction.EscapeFromEnvironment(body, state)) } diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 07c9ed581..4a44e36d6 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -288,7 +288,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { * @return synchronized reference to the globally unique identifier system */ def GUID(hub: NumberPoolHub): Boolean = { - if (actor == Default.typed.Actor && guid.Pools.values.foldLeft(0)(_ + _.Count) == 0) { + if (!zoneInitialized.isCompleted && guid.Pools.values.foldLeft(0)(_ + _.Count) == 0) { import org.fusesource.jansi.Ansi.Color.RED import org.fusesource.jansi.Ansi.ansi println( diff --git a/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala index 3c4a92a53..f5c0197b7 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala @@ -9,6 +9,9 @@ import net.psforever.objects.serverobject.deploy.Interference import net.psforever.objects.sourcing.ObjectSource import net.psforever.objects.vehicles.MountedWeapons import net.psforever.objects.vital.SpawningActivity +import net.psforever.packet.game.ChatMsg +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.types.ChatMessageType import scala.annotation.tailrec import scala.collection.mutable @@ -103,7 +106,13 @@ object ZoneDeployableActor { ): Boolean = { val position = obj.Position deployableList.find(_ eq obj) match { - case None if Interference.Test(zone, obj).isEmpty => + case _ if Interference.Test(zone, obj).nonEmpty => + zone.LocalEvents ! LocalServiceMessage( + obj.OwnerName.getOrElse(""), + LocalAction.SendResponse(ChatMsg(ChatMessageType.UNK_227, "@nomove_intersecting")) + ) //may not be the correct message but is sufficient at explaining why the deployable can not be built + false + case None => deployableList += obj zone.actor ! ZoneActor.AddToBlockMap(obj, position) true diff --git a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala index c1b7780be..4e05d85d3 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala @@ -3,11 +3,14 @@ package net.psforever.objects.zones import akka.actor.Actor import net.psforever.actors.zone.ZoneActor -import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.definition.{ObjectDefinition, VehicleDefinition} import net.psforever.objects.serverobject.deploy.{Deployment, Interference} import net.psforever.objects.vital.InGameHistory import net.psforever.objects.{Default, Vehicle} -import net.psforever.types.{DriveState, PlanetSideEmpire, Vector3} +import net.psforever.packet.game.ChatMsg +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{ChatMessageType, DriveState, PlanetSideEmpire, Vector3} import scala.annotation.tailrec import scala.collection.mutable @@ -66,15 +69,18 @@ class ZoneVehicleActor( sender() ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find") } - case Zone.Vehicle.TryDeploymentChange(vehicle, toDeployState) - if toDeployState == DriveState.Deploying && - (ZoneVehicleActor.temporaryInterferenceTest(vehicle, temporaryInterference) || Interference.Test(zone, vehicle).nonEmpty) => - sender() ! Zone.Vehicle.CanNotDeploy(zone, vehicle, toDeployState, "blocked by a nearby entity") - - case Zone.Vehicle.TryDeploymentChange(vehicle, toDeployState) - if toDeployState == DriveState.Deploying => - tryAddToInterferenceField(vehicle.Position, vehicle.Faction, vehicle.Definition) - vehicle.Actor.tell(Deployment.TryDeploymentChange(toDeployState), sender()) + case Zone.Vehicle.TryDeploymentChange(vehicle, DriveState.Deploying) => + if (ZoneVehicleActor.ReportOnInterferenceResults( + zone, + vehicle, + ZoneVehicleActor.temporaryInterferenceTest(vehicle, temporaryInterference) ++ + Interference.Test(zone, vehicle).map(_.Definition) + )) { + sender() ! Zone.Vehicle.CanNotDeploy(zone, vehicle, DriveState.Deploying, "blocked by a nearby entity") + } else { + tryAddToInterferenceField(vehicle.Position, vehicle.Faction, vehicle.Definition) + vehicle.Actor.tell(Deployment.TryDeploymentChange(DriveState.Deploying), sender()) + } case Zone.Vehicle.TryDeploymentChange(vehicle, toDeployState) => vehicle.Actor.tell(Deployment.TryDeploymentChange(toDeployState), sender()) @@ -83,7 +89,19 @@ class ZoneVehicleActor( case Zone.Vehicle.CanNotDespawn(_, _, _) => () - case Zone.Vehicle.CanNotDeploy(_, vehicle, _, reason) => () + case Zone.Vehicle.CanNotDeploy(_, vehicle, DriveState.Deploying, reason) => + ZoneVehicleActor.ReportOnInterferenceResults( + zone, + vehicle, + ZoneVehicleActor.temporaryInterferenceTest(vehicle, temporaryInterference) ++ + Interference.Test(zone, vehicle).map(_.Definition) + ) + val pos = vehicle.Position + val driverMoniker = vehicle.Seats.headOption.flatMap(_._2.occupant).map(_.Name).getOrElse("Driver") + log.warn(s"$driverMoniker's ${vehicle.Definition.Name} can not deploy in ${zone.id} because $reason") + temporaryInterference = temporaryInterference.filterNot(_._1 == pos) + + case Zone.Vehicle.CanNotDeploy(_, vehicle, _, reason) => val pos = vehicle.Position val driverMoniker = vehicle.Seats.headOption.flatMap(_._2.occupant).map(_.Name).getOrElse("Driver") log.warn(s"$driverMoniker's ${vehicle.Definition.Name} can not deploy in ${zone.id} because $reason") @@ -133,22 +151,54 @@ object ZoneVehicleActor { private def temporaryInterferenceTest( vehicle: Vehicle, existingInterferences: Seq[(Vector3, PlanetSideEmpire.Value, VehicleDefinition)] - ): Boolean = { + ): Seq[VehicleDefinition] = { val vPosition = vehicle.Position val vFaction = vehicle.Faction val vDefinition = vehicle.Definition if (vDefinition.interference eq Interference.AllowAll) { - false + Nil } else { existingInterferences .collect { case (p, faction, d) if faction == vFaction => (p, d) } - .exists { case (position, definition) => + .filter { case (position, definition) => val interference = definition.interference (interference ne Interference.AllowAll) && { lazy val distanceSq = Vector3.DistanceSquared(position, vPosition) definition == vDefinition && distanceSq < interference.main * interference.main } } + .map(_._2) + } + } + + private def ReportOnInterferenceResults( + zone: Zone, + vehicle: Vehicle, + reportedInterferenceList: Seq[ObjectDefinition] + ): Boolean = { + if (reportedInterferenceList.nonEmpty) { + reportedInterferenceList + .find(_.isInstanceOf[VehicleDefinition]) + .map { definition => s"@nodeploy_${definition.Name}" } + .orElse { + val sharedGroupId = vehicle.Definition.interference.sharedGroupId + if (sharedGroupId > 0) { + reportedInterferenceList + .find(_.interference.sharedGroupId == sharedGroupId) + .map(_ => "@nodeploy_sharedinterference") + } else { + None + } + } + .foreach { msg => + zone.VehicleEvents ! VehicleServiceMessage( + vehicle.Seats.headOption.flatMap(_._2.occupant).map(_.Name).getOrElse(""), + VehicleAction.SendResponse(Service.defaultPlayerGUID, ChatMsg(ChatMessageType.UNK_227, msg)) + ) + } + true + } else { + false } } } diff --git a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala index d80f16d77..0a7629a70 100644 --- a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala @@ -134,7 +134,17 @@ object OxygenStateMessage extends Marshallable[OxygenStateMessage] { 204.8f, 11 ) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode - OxygenState.codec + bool.xmap[OxygenState]( + { + case false => OxygenState.Recovery + case true => OxygenState.Suffocation + }, + { + case OxygenState.Recovery => false + case OxygenState.Suffocation => true + case _ => false + } + ) ).as[DrowningTarget] implicit val codec: Codec[OxygenStateMessage] = ( diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index b4b2939ed..1be70996c 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -1216,11 +1216,11 @@ object ObjectClass { case ObjectClass.advanced_ace => DroppedItemData(HandheldData.codec, "advanced ace") case ObjectClass.router_telepad => DroppedItemData(HandheldData.codec, "router telepad") case ObjectClass.boomer_trigger => DroppedItemData(HandheldData.codec, "boomer trigger") - case ObjectClass.boomer => ConstructorData(CommonFieldDataWithPlacement.codec, "ace deployable") - case ObjectClass.he_mine => ConstructorData(CommonFieldDataWithPlacement.codec, "ace deployable") - case ObjectClass.jammer_mine => ConstructorData(CommonFieldDataWithPlacement.codec, "ace deployable") - case ObjectClass.motionalarmsensor => ConstructorData(CommonFieldDataWithPlacement.codec, "ace deployable") - case ObjectClass.sensor_shield => ConstructorData(CommonFieldDataWithPlacement.codec, "ace deployable") + case ObjectClass.boomer => ConstructorData(SmallDeployableData.codec, "ace deployable") + case ObjectClass.he_mine => ConstructorData(SmallDeployableData.codec, "ace deployable") + case ObjectClass.jammer_mine => ConstructorData(SmallDeployableData.codec, "ace deployable") + case ObjectClass.motionalarmsensor => ConstructorData(SmallDeployableData.codec, "ace deployable") + case ObjectClass.sensor_shield => ConstructorData(SmallDeployableData.codec, "ace deployable") case ObjectClass.spitfire_aa => ConstructorData(SmallTurretData.codec, "small turret") case ObjectClass.spitfire_cloaked => ConstructorData(SmallTurretData.codec, "small turret") case ObjectClass.spitfire_turret => ConstructorData(SmallTurretData.codec, "small turret") diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/SmallDeployableData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/SmallDeployableData.scala new file mode 100644 index 000000000..fb08485b1 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/objectcreate/SmallDeployableData.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2024 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class SmallDeployableData(deploy: CommonFieldDataWithPlacement) extends ConstructorData { + override def bitsize: Long = { + deploy.bitsize + 1 + } +} + +object SmallDeployableData extends Marshallable[SmallDeployableData] { + implicit val codec: Codec[SmallDeployableData] = ( + ("deploy" | CommonFieldDataWithPlacement.codec) :: + ignore(size = 1) + ).exmap[SmallDeployableData]( + { + case deploy :: _ :: HNil => + Attempt.successful(SmallDeployableData(deploy)) + + case data => + Attempt.failure(Err(s"invalid small deployable data format - $data")) + }, + { + case SmallDeployableData(deploy) => + Attempt.successful(deploy :: () :: HNil) + } + ) +} diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 72aa05aa8..91d900d3b 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -120,6 +120,8 @@ class LocalService(zone: Zone) extends Actor { hackCapturer ! HackCaptureActor.StartCaptureTerminalHack(target, zone, 0, 8L) case LocalAction.LluCaptured(llu) => hackCapturer ! HackCaptureActor.FlagCaptured(llu) + case LocalAction.LluLost(llu) => + hackCapturer ! HackCaptureActor.FlagLost(llu) case LocalAction.LluSpawned(player_guid, llu) => // Forward to all clients to create object locally diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index e6d70f3af..de4545cc5 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -61,6 +61,7 @@ object LocalAction { final case class ResecureCaptureTerminal(target: CaptureTerminal, hacker: PlayerSource) extends Action final case class StartCaptureTerminalHack(target: CaptureTerminal) extends Action final case class LluCaptured(llu: CaptureFlag) extends Action + final case class LluLost(llu: CaptureFlag) extends Action final case class LluSpawned(player_guid: PlanetSideGUID, llu: CaptureFlag) extends Action final case class LluDespawned(player_guid: PlanetSideGUID, guid: PlanetSideGUID, position: Vector3) extends Action diff --git a/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala index 91bf2499f..df0d5f129 100644 --- a/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala +++ b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala @@ -3,12 +3,14 @@ package net.psforever.services.local.support import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.login.WorldSession -import net.psforever.objects.{Default, Player} +import net.psforever.objects.{Default, PlanetSideGameObject, Player} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} +import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait} +import net.psforever.objects.serverobject.environment.interaction.InteractWithEnvironment import net.psforever.objects.serverobject.llu.CaptureFlag -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, WarpGate} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal -import net.psforever.objects.zones.Zone +import net.psforever.objects.zones.{InteractsWithZone, Zone} import net.psforever.packet.game._ import net.psforever.services.{Service, ServiceManager} import net.psforever.services.ServiceManager.{Lookup, LookupResult} @@ -22,12 +24,13 @@ import scala.concurrent.duration.DurationInt * Responsible for handling capture flag related lifecycles */ class CaptureFlagManager(zone: Zone) extends Actor { + import CaptureFlagManager.CaptureFlagEntry private[this] val log = org.log4s.getLogger(self.path.name) private var galaxyService: ActorRef = ActorRef.noSender private var mapUpdateTick: Cancellable = Default.Cancellable /** An internally tracked list of cached flags, to avoid querying the amenity owner each second for flag lookups. */ - private var flags: List[CaptureFlag] = Nil + private var flags: List[CaptureFlagEntry] = Nil ServiceManager.serviceManager ! Lookup("galaxy") @@ -39,7 +42,6 @@ class CaptureFlagManager(zone: Zone) extends Actor { DoMapUpdate() case CaptureFlagManager.SpawnCaptureFlag(capture_terminal, target, hackingFaction) => - val zone = capture_terminal.Zone val socket = capture_terminal.Owner.asInstanceOf[Building].GetFlagSocket.get // Override CC message when looked at zone.LocalEvents ! LocalServiceMessage( @@ -62,16 +64,16 @@ class CaptureFlagManager(zone: Zone) extends Actor { socket.captureFlag = flag TrackFlag(flag) TaskWorkflow.execute(WorldSession.CallBackForTask( - GUIDTask.registerObject(socket.Zone.GUID, flag), - socket.Zone.LocalEvents, + GUIDTask.registerObject(zone.GUID, flag), + zone.LocalEvents, LocalServiceMessage( - socket.Zone.id, + zone.id, LocalAction.LluSpawned(Service.defaultPlayerGUID, flag) ) )) // Broadcast chat message for LLU spawn val owner = flag.Owner.asInstanceOf[Building] - ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_FlagSpawned(owner, flag.Target)) + CaptureFlagManager.ChatBroadcast(zone, CaptureFlagChatMessageStrings.CTF_FlagSpawned(owner, flag.Target)) case CaptureFlagManager.Captured(flag: CaptureFlag) => val name = flag.Carrier match { @@ -79,36 +81,46 @@ class CaptureFlagManager(zone: Zone) extends Actor { case None => "A soldier" } // Trigger Install sound - flag.Zone.LocalEvents ! LocalServiceMessage(flag.Zone.id, LocalAction.TriggerSound(PlanetSideGUID(-1), TriggeredSound.LLUInstall, flag.Target.CaptureTerminal.get.Position, 20, 0.8000001f)) + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.TriggerSound(PlanetSideGUID(-1), TriggeredSound.LLUInstall, flag.Target.CaptureTerminal.get.Position, 20, 0.8000001f)) // Broadcast capture chat message - ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_Success(name, flag.Faction, flag.Owner.asInstanceOf[Building].Name)) + CaptureFlagManager.ChatBroadcast(zone, CaptureFlagChatMessageStrings.CTF_Success(name, flag.Faction, flag.Owner.asInstanceOf[Building].Name)) // Despawn flag HandleFlagDespawn(flag) case CaptureFlagManager.Lost(flag: CaptureFlag, reason: CaptureFlagLostReasonEnum) => reason match { case CaptureFlagLostReasonEnum.Resecured => - ChatBroadcast( - flag.Zone, + CaptureFlagManager.ChatBroadcast( + zone, CaptureFlagChatMessageStrings.CTF_Failed_SourceResecured(flag.Owner.asInstanceOf[Building].Name, flag.Faction) ) case CaptureFlagLostReasonEnum.TimedOut => val building = flag.Owner.asInstanceOf[Building] - ChatBroadcast( - flag.Zone, + CaptureFlagManager.ChatBroadcast( + zone, CaptureFlagChatMessageStrings.CTF_Failed_TimedOut(building.Name, flag.Target.Name, flag.Faction) ) + case CaptureFlagLostReasonEnum.FlagLost => + val building = flag.Owner.asInstanceOf[Building] + CaptureFlagManager.ChatBroadcast( + zone, + CaptureFlagChatMessageStrings.CTF_Failed_FlagLost(building.Name, flag.Faction), + fanfare = false + ) case CaptureFlagLostReasonEnum.Ended => () } HandleFlagDespawn(flag) case CaptureFlagManager.PickupFlag(flag: CaptureFlag, player: Player) => - val continent = flag.Zone flag.Carrier = Some(player) - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.SendPacket(ObjectAttachMessage(player.GUID, flag.GUID, 252))) - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.TriggerSound(PlanetSideGUID(-1), TriggeredSound.LLUPickup, player.Position, 15, volume = 0.8f)) - ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_FlagPickedUp(player.Name, player.Faction, flag.Owner.asInstanceOf[Building].Name), fanfare = false) + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SendPacket(ObjectAttachMessage(player.GUID, flag.GUID, 252))) + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.TriggerSound(PlanetSideGUID(-1), TriggeredSound.LLUPickup, player.Position, 15, volume = 0.8f)) + CaptureFlagManager.ChatBroadcast( + zone, + CaptureFlagChatMessageStrings.CTF_FlagPickedUp(player.Name, player.Faction, flag.Owner.asInstanceOf[Building].Name), + fanfare = false + ) case CaptureFlagManager.DropFlag(flag: CaptureFlag) => flag.Carrier match { @@ -119,9 +131,13 @@ class CaptureFlagManager(zone: Zone) extends Actor { // Remove attached player from flag flag.Carrier = None // Send drop packet - flag.Zone.LocalEvents ! LocalServiceMessage(flag.Zone.id, LocalAction.SendPacket(ObjectDetachMessage(player.GUID, flag.GUID, player.Position, 0, 0, 0))) + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SendPacket(ObjectDetachMessage(player.GUID, flag.GUID, player.Position, 0, 0, 0))) // Send dropped chat message - ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_FlagDropped(player.Name, player.Faction, flag.Owner.asInstanceOf[Building].Name), fanfare = false) + CaptureFlagManager.ChatBroadcast( + zone, + CaptureFlagChatMessageStrings.CTF_FlagDropped(player.Name, player.Faction, flag.Owner.asInstanceOf[Building].Name), + fanfare = false + ) HandleFlagDespawn(flag) // Register LLU object create task and callback to create on clients val replacementLlu = CaptureFlag.Constructor( @@ -136,10 +152,10 @@ class CaptureFlagManager(zone: Zone) extends Actor { socket.captureFlag = replacementLlu TrackFlag(replacementLlu) TaskWorkflow.execute(WorldSession.CallBackForTask( - GUIDTask.registerObject(socket.Zone.GUID, replacementLlu), - socket.Zone.LocalEvents, + GUIDTask.registerObject(zone.GUID, replacementLlu), + zone.LocalEvents, LocalServiceMessage( - socket.Zone.id, + zone.id, LocalAction.LluSpawned(Service.defaultPlayerGUID, replacementLlu) ) )) @@ -152,33 +168,42 @@ class CaptureFlagManager(zone: Zone) extends Actor { } private def DoMapUpdate(): Unit = { - val flagInfo = flags.map(flag => + val flagInfo = flags.map { case entry @ CaptureFlagManager.CaptureFlagEntry(flag) => + val owner = flag.Owner.asInstanceOf[Building] + val pos = flag.Position + val hackTimeRemaining = owner.infoUpdateMessage().hack_time_remaining + val nextMessageAfterMinutes = CaptureFlagManager.CaptureFlagCountdownMessages(entry.currentMessageIndex) + if (hackTimeRemaining < nextMessageAfterMinutes.minutes.toMillis) { + entry.currentMessageIndex += 1 + val msg = CaptureFlagManager.ComposeWarningMessage(flag, owner.Name, nextMessageAfterMinutes) + CaptureFlagManager.ChatBroadcast(zone, msg, fanfare = false) + } FlagInfo( u1 = 0, - owner_map_id = flag.Owner.asInstanceOf[Building].MapId, + owner_map_id = owner.MapId, target_map_id = flag.Target.MapId, - x = flag.Position.x, - y = flag.Position.y, - hack_time_remaining = flag.Owner.asInstanceOf[Building].infoUpdateMessage().hack_time_remaining, + x = pos.x, + y = pos.y, + hack_time_remaining = hackTimeRemaining, is_monolith_unit = false ) - ) + } galaxyService ! GalaxyServiceMessage(GalaxyAction.FlagMapUpdate(CaptureFlagUpdateMessage(zone.Number, flagInfo))) } private def TrackFlag(flag: CaptureFlag): Unit = { flag.Owner.Amenities = flag - flags = flags :+ flag + flags = flags :+ CaptureFlagEntry(flag) if (mapUpdateTick.isCancelled) { // Start sending map updates periodically import scala.concurrent.ExecutionContext.Implicits.global - mapUpdateTick = context.system.scheduler.scheduleAtFixedRate(0 seconds, 1 second, self, CaptureFlagManager.MapUpdate()) + mapUpdateTick = context.system.scheduler.scheduleAtFixedRate(initialDelay = 0 seconds, interval = 1 second, self, CaptureFlagManager.MapUpdate()) } } private def UntrackFlag(flag: CaptureFlag): Unit = { flag.Owner.RemoveAmenity(flag) - flags = flags.filterNot(x => x eq flag) + flags = flags.filterNot(x => x.flag eq flag) if (flags.isEmpty) { mapUpdateTick.cancel() DoMapUpdate() @@ -186,7 +211,6 @@ class CaptureFlagManager(zone: Zone) extends Actor { } private def HandleFlagDespawn(flag: CaptureFlag): Unit = { - val zone = flag.Zone // Remove the flag as an amenity flag.Owner.asInstanceOf[Building].GetFlagSocket.get.captureFlag = None UntrackFlag(flag) @@ -195,8 +219,27 @@ class CaptureFlagManager(zone: Zone) extends Actor { // Then unregister it from the GUID pool TaskWorkflow.execute(GUIDTask.unregisterObject(zone.GUID, flag)) } +} + +object CaptureFlagManager { + sealed trait Command + + final case class SpawnCaptureFlag(capture_terminal: CaptureTerminal, target: Building, hackingFaction: PlanetSideEmpire.Value) extends Command + final case class PickupFlag(flag: CaptureFlag, player: Player) extends Command + final case class DropFlag(flag: CaptureFlag) extends Command + final case class Captured(flag: CaptureFlag) extends Command + final case class Lost(flag: CaptureFlag, reason: CaptureFlagLostReasonEnum) extends Command + final case class MapUpdate() + + private case class CaptureFlagEntry(flag: CaptureFlag) { + var currentMessageIndex: Int = 0 + } + + val CaptureFlagCountdownMessages: Seq[Int] = Seq(10, 5, 2, 1, 0) private def ChatBroadcast(zone: Zone, message: String, fanfare: Boolean = true): Unit = { + //todo use UNK_222 sometimes + //todo I think the fanfare was relate to whether the message was celebratory is tone, based on the faction val messageType: ChatMessageType = if (fanfare) { ChatMessageType.UNK_223 } else { @@ -206,33 +249,79 @@ class CaptureFlagManager(zone: Zone) extends Actor { zone.id, LocalAction.SendChatMsg( PlanetSideGUID(-1), - ChatMsg(messageType, wideContents = false, "", message, None) + ChatMsg(messageType, wideContents = true, "", message, None) ) ) } -} -object CaptureFlagManager { - sealed trait Command + private def ComposeWarningMessage(flag: CaptureFlag, buildingName: String, minutesLeft: Int): String = { + import CaptureFlagChatMessageStrings._ + val carrier = flag.Carrier + val hasCarrier = carrier.nonEmpty + minutesLeft match { + case 1 if hasCarrier => + CTF_Warning_Carrier1Min(carrier.get.Name, flag.Faction, buildingName, flag.Target.Name) + case 1 => + CTF_Warning_NoCarrier1Min(buildingName, flag.Faction, flag.Target.Name) + case time if hasCarrier => + CTF_Warning_Carrier(carrier.get.Name, flag.Faction, buildingName, flag.Target.Name, time) + case time => + CTF_Warning_NoCarrier(buildingName, flag.Faction, flag.Target.Name, time) + } + } - final case class SpawnCaptureFlag(capture_terminal: CaptureTerminal, target: Building, hackingFaction: PlanetSideEmpire.Value) extends Command - final case class PickupFlag(flag: CaptureFlag, player: Player) extends Command - final case class DropFlag(flag: CaptureFlag) extends Command - final case class Captured(flag: CaptureFlag) extends Command - final case class Lost(flag: CaptureFlag, reason: CaptureFlagLostReasonEnum) extends Command - final case class MapUpdate() + /** + * na + * @param flagGuid flag that may exist + * @param target evaluate this to determine if to continue with this loss + */ + def ReasonToLoseFlagViolently( + zone: Zone, + flagGuid: Option[PlanetSideGUID], + target: PlanetSideGameObject with InteractsWithZone + ): Boolean = { + zone + .GUID(flagGuid) + .collect { + case flag: CaptureFlag + if LoseFlagViolentlyToEnvironment(target, Set(EnvironmentAttribute.Water, EnvironmentAttribute.Lava, EnvironmentAttribute.Death)) || + LoseFlagViolentlyToWarpGateEnvelope(zone, target) => + flag.Destroyed = true + zone.LocalEvents ! LocalServiceMessage("", LocalAction.LluLost(flag)) + true + } + .getOrElse(false) + } + + def LoseFlagViolentlyToEnvironment( + target: PlanetSideGameObject with InteractsWithZone, + deniedEnvironments: Set[EnvironmentTrait] + ): Boolean = { + target + .interaction() + .collectFirst { case env: InteractWithEnvironment => env.OngoingInteractions } + .map(_.intersect(deniedEnvironments)) + .getOrElse(Set()) + .nonEmpty + } + + def LoseFlagViolentlyToWarpGateEnvelope( + zone: Zone, + target: PlanetSideGameObject with InteractsWithZone + ): Boolean = { + val position = target.Position + zone + .blockMap + .sector(position, range = 10f) + .buildingList + .collectFirst { + case gate: WarpGate if Vector3.DistanceSquared(position, gate.Position) < math.pow(gate.Definition.SOIRadius, 2f) => gate + } + .nonEmpty + } } object CaptureFlagChatMessageStrings { - /* - @CTF_Failed_TargetLost=%1's LLU target facility %2 was lost!\nHack canceled! - @CTF_Failed_FlagLost=The %1 lost %2's LLU!\nHack canceled! - @CTF_Warning_Carrier=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within %5 minutes! - @CTF_Warning_NoCarrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes! - @CTF_Warning_Carrier1Min=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within the next minute! - @CTF_Warning_NoCarrier1Min=%1's LLU is in the field.\nThe %2 must take it to %3 within the next minute! - */ - // @CTF_Success=%1 captured %2's LLU for the %3! /** {player.Name} captured {ownerName}'s LLU for the {player.Faction}! */ private[support] def CTF_Success(playerName:String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = @@ -243,10 +332,20 @@ object CaptureFlagChatMessageStrings { private[support] def CTF_Failed_TimedOut(ownerName: String, name: String, faction: PlanetSideEmpire.Value): String = s"@CTF_Failed_TimedOut^@${GetFactionString(faction)}~^@$ownerName~^@$name~" + // @CTF_Failed_Lost=The %1 lost %2's LLU!\nHack canceled! + /** The {faction} lost {ownerName}'s LLU!\nHack canceled! */ + private[support] def CTF_Failed_FlagLost(ownerName: String, faction: PlanetSideEmpire.Value): String = + s"@CTF_Failed_FlagLost^@${GetFactionString(faction)}~^@$ownerName~" + + // @CTF_Failed_TargetLost=%1's LLU target facility %2 was lost!\nHack canceled! + /** {hackFacility}'s LLU target facility {targetFacility} was lost!\nHack canceled! */ + private[support] def CTF_Failed_TargetLost(hackFacility: String, targetFacility: String): String = + s"@CTF_Failed_TargetLost^@$hackFacility~^@$targetFacility~" + // @CTF_Failed_SourceResecured=The %1 resecured %2!\nThe LLU was lost! /** The {faction} resecured {name}!\nThe LLU was lost! */ private[support] def CTF_Failed_SourceResecured(name: String, faction: PlanetSideEmpire.Value): String = - s"@CTF_Failed_SourceResecured^@${CaptureFlagChatMessageStrings.GetFactionString(faction)}~^@$name~" + s"@CTF_Failed_SourceResecured^@${GetFactionString(faction)}~^@$name~" // @CTF_FlagSpawned=%1 %2 has spawned a LLU.\nIt must be taken to %3 %4's Control Console within %5 minutes or the hack will fail! /** {facilityType} {facilityName} has spawned a LLU.\nIt must be taken to {targetFacilityType} {targetFacilityName}'s Control Console within 15 minutes or the hack will fail! */ @@ -255,13 +354,57 @@ object CaptureFlagChatMessageStrings { // @CTF_FlagPickedUp=%1 of the %2 picked up %3's LLU /** {player.Name} of the {player.Faction} picked up {ownerName}'s LLU */ - def CTF_FlagPickedUp(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = - s"@CTF_FlagPickedUp^$playerName~^@${CaptureFlagChatMessageStrings.GetFactionString(playerFaction)}~^@$ownerName~" + private[support] def CTF_FlagPickedUp(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = + s"@CTF_FlagPickedUp^$playerName~^@${GetFactionString(playerFaction)}~^@$ownerName~" // @CTF_FlagDropped=%1 of the %2 dropped %3's LLU /** {playerName} of the {faction} dropped {facilityName}'s LLU */ - def CTF_FlagDropped(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = - s"@CTF_FlagDropped^$playerName~^@${CaptureFlagChatMessageStrings.GetFactionString(playerFaction)}~^@$ownerName~" + private[support] def CTF_FlagDropped(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = + s"@CTF_FlagDropped^$playerName~^@${GetFactionString(playerFaction)}~^@$ownerName~" + + // @CTF_Warning_Carrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes! + /** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within {time} minutes! */ + private[support] def CTF_Warning_Carrier( + playerName:String, + playerFaction: PlanetSideEmpire.Value, + facilityName: String, + targetFacilityName: String, + time: Int + ): String = { + s"@CTF_Warning_Carrier^$playerName~^@${GetFactionString(playerFaction)}~^@$facilityName~^@$targetFacilityName~^@$time~" + } + + // @CTF_Warning_Carrier1Min=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within the next minute! + /** {playerName} of the {faction} has {facilityName}'s LLU.\nIt must be taken to {targetFacilityName} within the next minute! */ + private[support] def CTF_Warning_Carrier1Min( + playerName:String, + playerFaction: PlanetSideEmpire.Value, + facilityName: String, + targetFacilityName: String + ): String = { + s"@CTF_Warning_Carrier1Min^$playerName~^@${GetFactionString(playerFaction)}~^@$facilityName~^@$targetFacilityName~" + } + + // @CTF_Warning_NoCarrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes! + /** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within {time} minute! */ + private[support] def CTF_Warning_NoCarrier( + facilityName: String, + playerFaction: PlanetSideEmpire.Value, + targetFacilityName: String, + time: Int + ): String = { + s"@CTF_Warning_NoCarrier^@$facilityName~^@${GetFactionString(playerFaction)}~^@$targetFacilityName~^$time~" + } + + // @CTF_Warning_NoCarrier1Min=%1's LLU is in the field.\nThe %2 must take it to %3 within the next minute! + /** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within the next minute! */ + private[support] def CTF_Warning_NoCarrier1Min( + facilityName: String, + playerFaction: PlanetSideEmpire.Value, + targetFacilityName: String + ): String = { + s"@CTF_Warning_NoCarrier1Min^@$facilityName~^@${GetFactionString(playerFaction)}~^@$targetFacilityName~" + } private def GetFactionString: PlanetSideEmpire.Value=>String = { case PlanetSideEmpire.TR => "TerranRepublic" @@ -277,4 +420,5 @@ object CaptureFlagLostReasonEnum { final case object Resecured extends CaptureFlagLostReasonEnum final case object TimedOut extends CaptureFlagLostReasonEnum final case object Ended extends CaptureFlagLostReasonEnum + final case object FlagLost extends CaptureFlagLostReasonEnum } diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index b9a4f4700..1a88b0e00 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -6,16 +6,17 @@ import net.psforever.actors.zone.{BuildingActor, ZoneActor} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.llu.CaptureFlag -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.zones.Zone import net.psforever.objects.Default -import net.psforever.packet.game.{GenericAction, HackState7, PlanetsideAttributeEnum} +import net.psforever.objects.serverobject.structures.participation.MajorFacilityHackParticipation +import net.psforever.packet.game.{ChatMsg, GenericAction, HackState7, PlanetsideAttributeEnum} import net.psforever.objects.sourcing.PlayerSource import net.psforever.services.Service import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID} import scala.concurrent.duration.{FiniteDuration, _} import scala.util.Random @@ -62,6 +63,11 @@ class HackCaptureActor extends Actor { // If the base has a socket, but no flag spawned it means the hacked base is neutral with no friendly neighbouring bases to deliver to, making it a timed hack. val building = terminal.Owner.asInstanceOf[Building] building.GetFlag match { + case Some(llu) if llu.Destroyed => + // LLU was destroyed while in the field. Send resecured notifications + terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.FlagLost) + NotifyHackStateChange(terminal, isResecured = true) + case Some(llu) => // LLU was not delivered in time. Send resecured notifications terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.TimedOut) @@ -138,6 +144,28 @@ class HackCaptureActor extends Actor { log.error(s"Attempted LLU capture for ${flag.Owner.asInstanceOf[Building].Name} but CC GUID ${flag.Owner.asInstanceOf[Building].CaptureTerminal.get.GUID} was not in list of hacked objects") } + case HackCaptureActor.FlagLost(flag) => + val owner = flag.Owner.asInstanceOf[Building] + val guid = owner.GUID + val terminalOpt = owner.CaptureTerminal + hackedObjects + .find(entry => guid == entry.target.Owner.GUID) + .collect { entry => + val terminal = terminalOpt.get + hackedObjects = hackedObjects.filterNot(x => x eq entry) + log.info(s"FlagLost: ${flag.Carrier.map(_.Name).getOrElse("")} the flag carrier screwed up the capture for ${flag.Target.Name} and the LLU has been lost") + terminal.Actor ! CommonMessages.ClearHack() + NotifyHackStateChange(terminal, isResecured = true) + // If there's hacked objects left in the list restart the timer with the shortest hack time left + RestartTimer() + entry + } + .orElse{ + log.warn(s"FlagLost: flag data does not match to an entry in the hacked objects list") + None + } + context.parent ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.FlagLost) + case _ => () } @@ -209,6 +237,18 @@ class HackCaptureActor extends Actor { val owner = terminal.Owner.asInstanceOf[Building] // Notify parent building that state has changed owner.Actor ! BuildingActor.AmenityStateChange(terminal, Some(isResecured)) + // If major facility, check NTU + owner.CaptureTerminal + .map(_.HackedBy) + .collect { + case Some(info: Hackable.HackInfo) + if owner.BuildingType == StructureType.Facility && owner.NtuLevel == 0 => + MajorFacilityHackParticipation.warningMessageForHackOccupiers( + owner, + info, + ChatMsg(ChatMessageType.UNK_227, "@FacilityRequiresResourcesForHackWarning") + ) + } // Push map update to clients owner.Zone.actor ! ZoneActor.ZoneMapUpdate() } @@ -256,6 +296,7 @@ object HackCaptureActor { final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource) final case class FlagCaptured(flag: CaptureFlag) + final case class FlagLost(flag: CaptureFlag) private final case class ProcessCompleteHacks() diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala index 35b377981..72168faa0 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala @@ -184,6 +184,10 @@ class VehicleService(zone: Zone) extends Actor { VehicleResponse.MountVehicle(vehicle_guid, seat) ) ) + case VehicleAction.LoseOwnership(owner_guid, vehicle_guid) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$forChannel/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoseOwnership(owner_guid, vehicle_guid)) + ) case VehicleAction.Ownership(player_guid, vehicle_guid) => VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.Ownership(vehicle_guid)) diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala index 979d179f9..07bb29769 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala @@ -93,6 +93,7 @@ object VehicleAction { ) extends Action final case class MountVehicle(player_guid: PlanetSideGUID, object_guid: PlanetSideGUID, seat: Int) extends Action final case class ObjectDelete(guid: PlanetSideGUID) extends Action + final case class LoseOwnership(owner_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID) extends Action final case class Ownership(player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID) extends Action final case class PlanetsideAttribute( player_guid: PlanetSideGUID, diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala index e64c6536e..22e2ed94f 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala @@ -74,6 +74,8 @@ object VehicleResponse { final case class MountVehicle(object_guid: PlanetSideGUID, seat: Int) extends Response final case class ObjectDelete(guid: PlanetSideGUID) extends Response final case class Ownership(vehicle_guid: PlanetSideGUID) extends Response + final case class LoseOwnership(owner_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID) + extends Response final case class PlanetsideAttribute(vehicle_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) extends Response final case class Reload(weapon_guid: PlanetSideGUID) extends Response diff --git a/src/main/scala/net/psforever/types/OxygenState.scala b/src/main/scala/net/psforever/types/OxygenState.scala index 56f27166a..c588a2ac7 100644 --- a/src/main/scala/net/psforever/types/OxygenState.scala +++ b/src/main/scala/net/psforever/types/OxygenState.scala @@ -1,9 +1,6 @@ package net.psforever.types import enumeratum.{Enum, EnumEntry} -import net.psforever.packet.PacketHelpers -import scodec.Codec -import scodec.codecs.uint /** * The progress state of being a drowning victim. @@ -22,6 +19,4 @@ object OxygenState extends Enum[OxygenState] { case object Recovery extends OxygenState case object Suffocation extends OxygenState - - implicit val codec: Codec[OxygenState] = PacketHelpers.createEnumCodec(e = this, uint(bits = 1)) } diff --git a/src/test/scala/game/objectcreate/CommonFieldDataWithPlacementTest.scala b/src/test/scala/game/objectcreate/CommonFieldDataWithPlacementTest.scala index dd5a89fde..f3442112e 100644 --- a/src/test/scala/game/objectcreate/CommonFieldDataWithPlacementTest.scala +++ b/src/test/scala/game/objectcreate/CommonFieldDataWithPlacementTest.scala @@ -20,7 +20,7 @@ class CommonFieldDataWithPlacementTest extends Specification { guid mustEqual PlanetSideGUID(3840) parent.isDefined mustEqual false data match { - case CommonFieldDataWithPlacement(pos, com) => + case SmallDeployableData(CommonFieldDataWithPlacement(pos, com)) => pos.coord mustEqual Vector3(4704.172f, 5546.4375f, 82.234375f) pos.orient mustEqual Vector3.z(272.8125f) com match { @@ -31,9 +31,9 @@ class CommonFieldDataWithPlacementTest extends Specification { v1 mustEqual false v2.isEmpty mustEqual true v3 mustEqual false - v4.contains(false) mustEqual true + v4.isEmpty mustEqual true v5.isEmpty mustEqual true - fguid mustEqual PlanetSideGUID(8290) + fguid mustEqual PlanetSideGUID(4145) case _ => ko } @@ -46,10 +46,10 @@ class CommonFieldDataWithPlacementTest extends Specification { } "encode" in { - val obj = CommonFieldDataWithPlacement( + val obj = SmallDeployableData(CommonFieldDataWithPlacement( PlacementData(Vector3(4704.172f, 5546.4375f, 82.234375f), Vector3.z(272.8125f)), - CommonFieldData(PlanetSideEmpire.TR, false, false, false, None, false, Some(false), None, PlanetSideGUID(8290)) - ) + CommonFieldData(PlanetSideEmpire.TR, bops = false, alternate = false, v1 = false, None, jammered = false, None, None, PlanetSideGUID(4145)) + )) val msg = ObjectCreateMessage(ObjectClass.boomer, PlanetSideGUID(3840), obj) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string_boomer diff --git a/src/test/scala/objects/ConverterTest.scala b/src/test/scala/objects/ConverterTest.scala index 691a06e77..e25352925 100644 --- a/src/test/scala/objects/ConverterTest.scala +++ b/src/test/scala/objects/ConverterTest.scala @@ -32,9 +32,9 @@ class ConverterTest extends Specification { PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, - true, + v1 = true, None, - false, + jammered = false, None, None, PlanetSideGUID(0) @@ -50,9 +50,9 @@ class ConverterTest extends Specification { PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, - false, + v1 = false, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -71,7 +71,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(pkt) => pkt mustEqual DetailedWeaponData( - CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)), + CommonFieldData(PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, v1 = true, None, jammered = false, None, None, PlanetSideGUID(0)), 0, List(InternalSlot(Ammo.shotgun_shell.id, PlanetSideGUID(90), 0, DetailedAmmoBoxData(8, 12))) ) @@ -81,7 +81,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.ConstructorData(obj) match { case Success(pkt) => pkt mustEqual WeaponData( - CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)), + CommonFieldData(PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, v1 = true, None, jammered = false, None, None, PlanetSideGUID(0)), 0, List( InternalSlot( @@ -90,11 +90,11 @@ class ConverterTest extends Specification { 0, CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - false, + bops = false, + alternate = false, + v1 = false, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -115,7 +115,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(pkt) => pkt mustEqual DetailedWeaponData( - CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)), + CommonFieldData(PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, v1 = true, None, jammered = false, None, None, PlanetSideGUID(0)), 0, List( InternalSlot(Ammo.bullet_9mm.id, PlanetSideGUID(90), 0, DetailedAmmoBoxData(8, 30)), @@ -132,17 +132,17 @@ class ConverterTest extends Specification { PlanetSideEmpire.NEUTRAL, //TODO need faction affinity bops = false, alternate = false, - true, + v1 = true, None, - false, + jammered = false, None, None, PlanetSideGUID(0) ), 0, List( - InternalSlot(Ammo.bullet_9mm.id, PlanetSideGUID(90), 0, CommonFieldData()(false)), - InternalSlot(Ammo.rocket.id, PlanetSideGUID(91), 1, CommonFieldData()(false)) + InternalSlot(Ammo.bullet_9mm.id, PlanetSideGUID(90), 0, CommonFieldData()(flag = false)), + InternalSlot(Ammo.rocket.id, PlanetSideGUID(91), 1, CommonFieldData()(flag = false)) ) ) case _ => @@ -164,7 +164,7 @@ class ConverterTest extends Specification { } obj.Definition.Packet.ConstructorData(obj) match { case Success(pkt) => - pkt mustEqual CommonFieldData()(false) + pkt mustEqual CommonFieldData()(flag = false) case _ => ko } @@ -177,7 +177,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(pkt) => pkt mustEqual DetailedConstructionToolData( - CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)) + CommonFieldData(PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, v1 = true, None, jammered = false, None, None, PlanetSideGUID(0)) ) case _ => ko @@ -188,11 +188,11 @@ class ConverterTest extends Specification { pkt mustEqual HandheldData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, None, None, PlanetSideGUID(0) @@ -216,11 +216,11 @@ class ConverterTest extends Specification { pkt mustEqual DetailedREKData( CommonFieldData( PlanetSideEmpire.NEUTRAL, //TODO faction affinity - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -234,11 +234,11 @@ class ConverterTest extends Specification { pkt mustEqual REKData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -257,7 +257,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(pkt) => pkt mustEqual DetailedConstructionToolData( - CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)) + CommonFieldData(PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, v1 = true, None, jammered = false, None, None, PlanetSideGUID(0)) ) case _ => ko @@ -265,7 +265,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.ConstructorData(obj) match { case Success(pkt) => pkt mustEqual HandheldData( - CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, false, None, false, None, None, PlanetSideGUID(0)) + CommonFieldData(PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, v1 = false, None, jammered = false, None, None, PlanetSideGUID(0)) ) case _ => ko @@ -282,11 +282,11 @@ class ConverterTest extends Specification { pkt mustEqual HandheldData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - false, + bops = false, + alternate = false, + v1 = false, None, - false, + jammered = false, None, Some(1001), PlanetSideGUID(0) @@ -301,11 +301,11 @@ class ConverterTest extends Specification { pkt mustEqual DetailedConstructionToolData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, None, Some(1001), PlanetSideGUID(0) @@ -333,20 +333,20 @@ class ConverterTest extends Specification { obj.Definition.Packet.ConstructorData(obj) match { case Success(pkt) => - pkt mustEqual CommonFieldDataWithPlacement( + pkt mustEqual SmallDeployableData(CommonFieldDataWithPlacement( PlacementData(Vector3.Zero, Vector3.Zero), CommonFieldData( PlanetSideEmpire.TR, - false, - false, - false, + bops = false, + alternate = false, + v1 = false, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) ) - ) + )) case _ => ko } @@ -369,12 +369,12 @@ class ConverterTest extends Specification { PlacementData(Vector3.Zero, Vector3.Zero), CommonFieldData( PlanetSideEmpire.TR, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, + None, + jammered = false, None, - false, - Some(true), None, PlanetSideGUID(0) ) @@ -389,11 +389,11 @@ class ConverterTest extends Specification { WeaponData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, None, None, PlanetSideGUID(0) @@ -406,11 +406,11 @@ class ConverterTest extends Specification { 0, CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - false, + bops = false, + alternate = false, + v1 = false, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -444,11 +444,11 @@ class ConverterTest extends Specification { PlacementData(Vector3.Zero, Vector3.Zero), CommonFieldData( PlanetSideEmpire.TR, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -464,11 +464,11 @@ class ConverterTest extends Specification { WeaponData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, None, None, PlanetSideGUID(0) @@ -481,11 +481,11 @@ class ConverterTest extends Specification { 0, CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - false, + bops = false, + alternate = false, + v1 = false, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -519,10 +519,10 @@ class ConverterTest extends Specification { PlanetSideEmpire.TR, bops = false, alternate = false, - true, + v1 = true, + None, + jammered = false, None, - false, - Some(true), None, PlanetSideGUID(0) ) @@ -584,9 +584,9 @@ class ConverterTest extends Specification { PlanetSideEmpire.TR, bops = false, alternate = false, - true, + v1 = true, None, - false, + jammered = false, None, Some(1001), PlanetSideGUID(5001) @@ -616,9 +616,9 @@ class ConverterTest extends Specification { PlanetSideEmpire.TR, bops = false, alternate = true, - true, + v1 = true, None, - false, + jammered = false, None, Some(1001), PlanetSideGUID(0) @@ -751,7 +751,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(pkt) => pkt mustEqual DetailedLockerContainerData( - CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, false, None, false, None, None, PlanetSideGUID(0)), + CommonFieldData(PlanetSideEmpire.NEUTRAL, bops = false, alternate = false, v1 = false, None, jammered = false, None, None, PlanetSideGUID(0)), None ) case _ => @@ -783,11 +783,11 @@ class ConverterTest extends Specification { DetailedREKData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -809,11 +809,11 @@ class ConverterTest extends Specification { REKData( CommonFieldData( PlanetSideEmpire.NEUTRAL, - false, - false, - true, + bops = false, + alternate = false, + v1 = true, None, - false, + jammered = false, Some(false), None, PlanetSideGUID(0) @@ -841,7 +841,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.ConstructorData(obj) match { case Success(pkt) => - pkt mustEqual CommonFieldData(PlanetSideEmpire.NEUTRAL)(false) + pkt mustEqual CommonFieldData(PlanetSideEmpire.NEUTRAL)(flag = false) case _ => ko } @@ -861,7 +861,7 @@ class ConverterTest extends Specification { obj.Definition.Packet.ConstructorData(obj) match { case Success(pkt) => - pkt mustEqual CommonFieldData(PlanetSideEmpire.NEUTRAL)(false) + pkt mustEqual CommonFieldData(PlanetSideEmpire.NEUTRAL)(flag = false) case _ => ko } diff --git a/src/test/scala/objects/DefaultTest.scala b/src/test/scala/objects/DefaultTest.scala index cbf5ca7a5..3e1569aa0 100644 --- a/src/test/scala/objects/DefaultTest.scala +++ b/src/test/scala/objects/DefaultTest.scala @@ -21,26 +21,26 @@ class DefaultTest extends Specification { } } -class DefaultActorStartedTest extends ActorTest { - "Default.Actor" should { - "send messages to deadLetters" in { - //after being started - val probe = new TestProbe(system) - system.eventStream.subscribe(probe.ref, classOf[DeadLetter]) - Default.Actor ! "hello world" - val msg1 = probe.receiveOne(5000 milliseconds) - assert(msg1.isInstanceOf[DeadLetter]) - assert(msg1.asInstanceOf[DeadLetter].message equals "hello world") - - //if it was stopped - system.stop(Default.Actor) - Default.Actor ! "hello world" - val msg2 = probe.receiveOne(5000 milliseconds) - assert(msg2.isInstanceOf[DeadLetter]) - assert(msg2.asInstanceOf[DeadLetter].message equals "hello world") - } - } -} +//class DefaultActorStartedTest extends ActorTest { +// "Default.Actor" should { +// "send messages to deadLetters" in { +// //after being started +// val probe = new TestProbe(system) +// system.eventStream.subscribe(probe.ref, classOf[DeadLetter]) +// Default.Actor ! "hello world" +// val msg1 = probe.receiveOne(5000 milliseconds) +// assert(msg1.isInstanceOf[DeadLetter]) +// assert(msg1.asInstanceOf[DeadLetter].message equals "hello world") +// +// //if it was stopped +// system.stop(Default.Actor) +// Default.Actor ! "hello world" +// val msg2 = probe.receiveOne(5000 milliseconds) +// assert(msg2.isInstanceOf[DeadLetter]) +// assert(msg2.asInstanceOf[DeadLetter].message equals "hello world") +// } +// } +//} object DefaultActorTest { //due to being a singleton, the original original value of the Default.Actor is cached here diff --git a/src/test/scala/objects/DeployableBehaviorTest.scala b/src/test/scala/objects/DeployableBehaviorTest.scala index 0de9023c5..e9ac7df88 100644 --- a/src/test/scala/objects/DeployableBehaviorTest.scala +++ b/src/test/scala/objects/DeployableBehaviorTest.scala @@ -48,21 +48,14 @@ class DeployableBehaviorSetupTest extends ActorTest { assert(deployableList.isEmpty, "self-setup test - deployable list is not empty") zone.Deployables ! Zone.Deployable.Build(jmine) - val eventsMsgs = eventsProbe.receiveN(3, 10.seconds) + val eventsMsgs = eventsProbe.receiveN(2, 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 LocalServiceMessage("test", LocalAction.DeployItem(obj)) => assert(obj eq jmine, "self-setup test - not same mine") case _ => assert( false, "self-setup test - wrong deploy message") } - eventsMsgs(2) match { + eventsMsgs(1) match { case LocalServiceMessage( "TR", LocalAction.DeployableMapIcon( @@ -251,13 +244,13 @@ class DeployableBehaviorDeconstructTest extends ActorTest { "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) + eventsProbe.receiveN(2, 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(2, 10.seconds) eventsMsgs.head match { - case LocalServiceMessage("test", LocalAction.EliminateDeployable(`jmine`, PlanetSideGUID(1), Vector3(1,2,3), 2)) => ; + case LocalServiceMessage("test", LocalAction.EliminateDeployable(_, PlanetSideGUID(1), Vector3(1,2,3), 2)) => ; case _ => assert(false, "deconstruct test - not eliminating deployable") } eventsMsgs(1) match { diff --git a/src/test/scala/objects/DeployableTest.scala b/src/test/scala/objects/DeployableTest.scala index 1708ff74a..b02c1ed21 100644 --- a/src/test/scala/objects/DeployableTest.scala +++ b/src/test/scala/objects/DeployableTest.scala @@ -314,7 +314,7 @@ class ExplosiveDeployableJammerTest extends ActorTest { val guid = new NumberPoolHub(new MaxNumberSource(10)) val eventsProbe = new TestProbe(system) - val j_mine = Deployables.Make(DeployedItem.jammer_mine)().asInstanceOf[ExplosiveDeployable] //guid=1 + val mine = Deployables.Make(DeployedItem.he_mine)().asInstanceOf[ExplosiveDeployable] //guid=1 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) @@ -335,17 +335,17 @@ class ExplosiveDeployableJammerTest extends ActorTest { } player1.Spawn() player2.Spawn() - guid.register(j_mine, 1) + guid.register(mine, 1) guid.register(player1, 3) guid.register(player2, 4) guid.register(weapon, 5) - j_mine.Zone = zone - j_mine.OwnerGuid = player2 + mine.Zone = zone + mine.OwnerGuid = player2 //j_mine.OwnerName = player2.Name - j_mine.Faction = PlanetSideEmpire.NC - j_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], j_mine), "j-mine-control") + mine.Faction = PlanetSideEmpire.NC + mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], mine), "j-mine-control") - val jMineSource = SourceEntry(j_mine) + val jMineSource = SourceEntry(mine) val pSource = PlayerSource(player1) val projectile = weapon.Projectile val resolved = DamageInteraction( @@ -353,7 +353,7 @@ class ExplosiveDeployableJammerTest extends ActorTest { ProjectileReason( DamageResolution.Hit, Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), - j_mine.DamageModel + mine.DamageModel ), Vector3(1, 0, 0) ) @@ -361,26 +361,53 @@ class ExplosiveDeployableJammerTest extends ActorTest { "ExplosiveDeployable" should { "handle being jammered appropriately (no detonation)" in { - assert(!j_mine.Destroyed) + assert(!mine.Destroyed) - j_mine.Actor ! Vitality.Damage(applyDamageToJ) - val eventMsgs = eventsProbe.receiveN(2, 200 milliseconds) + mine.Actor ! Vitality.Damage(applyDamageToJ) + val eventMsgs = eventsProbe.receiveN(4, 200 milliseconds) eventMsgs.head match { + case Zone.HotSpot.Conflict(mineSrc, playerSrc, Vector3(1.0,0.0,0.0)) => ; + case _ => assert(false, "") + } +// eventMsgs.head match { +// case LocalServiceMessage( +// "NC", +// LocalAction.DeployableMapIcon( +// ValidPlanetSideGUID(0), +// DeploymentAction.Dismiss, +// DeployableInfo(ValidPlanetSideGUID(1), DeployableIcon.HEMine, Vector3.Zero, ValidPlanetSideGUID(0)) +// ) +// ) => ; +// case _ => assert(false, "") +// } + eventMsgs(1) match { + case LocalServiceMessage("test", LocalAction.Detonate(PlanetSideGUID(1), _)) => ; + case _ => assert(false, "") + } + eventMsgs(2) match { case LocalServiceMessage( "NC", LocalAction.DeployableMapIcon( ValidPlanetSideGUID(0), DeploymentAction.Dismiss, - DeployableInfo(ValidPlanetSideGUID(1), DeployableIcon.DisruptorMine, Vector3.Zero, ValidPlanetSideGUID(0)) + DeployableInfo(ValidPlanetSideGUID(1), DeployableIcon.HEMine, Vector3.Zero, ValidPlanetSideGUID(0)) ) ) => ; case _ => assert(false, "") } - eventMsgs(1) match { - case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(1), _, Service.defaultPlayerGUID, _)) => ; + eventMsgs(3) match { + case AvatarServiceMessage( + "test", + AvatarAction.Destroy( + ValidPlanetSideGUID(1), + ValidPlanetSideGUID(3), + ValidPlanetSideGUID(0), + Vector3.Zero + ) + ) => ; case _ => assert(false, "") } - assert(j_mine.Destroyed) + assert(mine.Destroyed) } } } diff --git a/src/test/scala/objects/DeploymentTest.scala b/src/test/scala/objects/DeploymentTest.scala index ca83ddc18..0b86da3f9 100644 --- a/src/test/scala/objects/DeploymentTest.scala +++ b/src/test/scala/objects/DeploymentTest.scala @@ -65,57 +65,59 @@ class DeploymentBehavior2Test extends ActorTest { assert(obj.DeploymentState == DriveState.Mobile) //to Deploying obj.Actor.tell(Deployment.TryDeploymentChange(DriveState.Deploying), probe.ref) - val reply1a = probe.receiveOne(Duration.create(500, "ms")) - assert(reply1a match { - case Deployment.CanDeploy(_, DriveState.Deploying) => true - case _ => false - }) - val reply1b = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply1b match { + val reply1 = probe.receiveN(2, Duration.create(2000, "ms")) + val reply2 = eventsProbe.receiveN(2, Duration.create(2000, "ms")) + reply1.head match { + case Deployment.CanDeploy(_, DriveState.Deploying) => () + case _ => assert(false, "") + } + reply2.head match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deploying, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deploying, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } //to Deployed - val reply2 = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply2 match { + reply1(1) match { + case Deployment.CanDeploy(_, DriveState.Deployed) => () + case _ => assert(false, "") + } + reply2(1) match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deployed, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deployed, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } assert(obj.DeploymentState == DriveState.Deployed) //to Undeploying obj.Actor.tell(Deployment.TryDeploymentChange(DriveState.Undeploying), probe.ref) - val reply3a = probe.receiveOne(Duration.create(500, "ms")) - assert(reply3a match { - case Deployment.CanUndeploy(_, DriveState.Undeploying) => true - case _ => false - }) - val reply3b = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply3b match { + val reply3 = probe.receiveN(2, Duration.create(2000, "ms")) + val reply4 = eventsProbe.receiveN(2, Duration.create(2000, "ms")) + reply3.head match { + case Deployment.CanUndeploy(_, DriveState.Undeploying) => () + case _ => assert(false, "") + } + reply4.head match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Undeploying, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Undeploying, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } //to Mobile - val reply4 = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply4 match { + reply3(1) match { + case Deployment.CanUndeploy(_, DriveState.Mobile) => () + case _ => assert(false, "") + } + reply4(1) match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Mobile, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Mobile, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } assert(obj.DeploymentState == DriveState.Mobile) } } @@ -131,57 +133,59 @@ class DeploymentBehavior3Test extends ActorTest { assert(obj.DeploymentState == DriveState.Mobile) //to Deploying obj.Actor.tell(Deployment.TryDeploy(DriveState.Deploying), probe.ref) - val reply1a = probe.receiveOne(Duration.create(500, "ms")) - assert(reply1a match { - case Deployment.CanDeploy(_, DriveState.Deploying) => true - case _ => false - }) - val reply1b = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply1b match { + val reply1 = probe.receiveN(2, Duration.create(2000, "ms")) + val reply2 = eventsProbe.receiveN(2, Duration.create(2000, "ms")) + reply1.head match { + case Deployment.CanDeploy(_, DriveState.Deploying) => () + case _ => assert(false, "") + } + reply2.head match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deploying, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deploying, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } //to Deployed - val reply2 = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply2 match { + reply1(1) match { + case Deployment.CanDeploy(_, DriveState.Deployed) => () + case _ => assert(false, "") + } + reply2(1) match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deployed, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Deployed, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } assert(obj.DeploymentState == DriveState.Deployed) //to Undeploying obj.Actor.tell(Deployment.TryUndeploy(DriveState.Undeploying), probe.ref) - val reply3a = probe.receiveOne(Duration.create(500, "ms")) - assert(reply3a match { - case Deployment.CanUndeploy(_, DriveState.Undeploying) => true - case _ => false - }) - val reply3b = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply3b match { + val reply3 = probe.receiveN(2, Duration.create(2000, "ms")) + val reply4 = eventsProbe.receiveN(2, Duration.create(2000, "ms")) + reply3.head match { + case Deployment.CanUndeploy(_, DriveState.Undeploying) => () + case _ => assert(false, "") + } + reply4.head match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Undeploying, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Undeploying, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } //to Mobile - val reply4 = eventsProbe.receiveOne(Duration.create(500, "ms")) - assert(reply4 match { + reply3(1) match { + case Deployment.CanUndeploy(_, DriveState.Mobile) => () + case _ => assert(false, "") + } + reply4(1) match { case VehicleServiceMessage( - "test", - VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Mobile, 0, false, Vector3.Zero) - ) => - true - case _ => false - }) + "test", + VehicleAction.DeployRequest(_, PlanetSideGUID(1), DriveState.Mobile, 0, false, Vector3.Zero) + ) => () + case _ => assert(false, "") + } assert(obj.DeploymentState == DriveState.Mobile) } } @@ -195,16 +199,18 @@ class DeploymentBehavior4Test extends ActorTest { obj.Actor ! Deployment.TryDeploymentChange(DriveState.Deployed) val reply1 = receiveOne(Duration.create(100, "ms")) - assert(reply1.isInstanceOf[Deployment.CanNotChangeDeployment]) - assert(reply1.asInstanceOf[Deployment.CanNotChangeDeployment].obj == obj) - assert(reply1.asInstanceOf[Deployment.CanNotChangeDeployment].to_state == DriveState.Deployed) + reply1 match { + case Deployment.CanNotChangeDeployment(_, DriveState.Deployed, _) => () + case _ => assert(false, "") + } assert(obj.DeploymentState == DriveState.Mobile) obj.Actor ! Deployment.TryDeploy(DriveState.Deployed) val reply2 = receiveOne(Duration.create(100, "ms")) - assert(reply2.isInstanceOf[Deployment.CanNotChangeDeployment]) - assert(reply2.asInstanceOf[Deployment.CanNotChangeDeployment].obj == obj) - assert(reply2.asInstanceOf[Deployment.CanNotChangeDeployment].to_state == DriveState.Deployed) + reply2 match { + case Deployment.CanNotChangeDeployment(_, DriveState.Deployed, _) => () + case _ => assert(false, "") + } assert(obj.DeploymentState == DriveState.Mobile) } } @@ -215,18 +221,22 @@ class DeploymentBehavior5Test extends ActorTest { "not deploy to an undeploy state" in { val obj = DeploymentTest.SetUpAgent assert(obj.DeploymentState == DriveState.Mobile) - obj.Actor ! Deployment.TryDeploymentChange(DriveState.Deploying) - receiveOne(Duration.create(100, "ms")) //consume - obj.Actor ! Deployment.TryDeploymentChange(DriveState.Deployed) - receiveOne(Duration.create(100, "ms")) //consume - assert(obj.DeploymentState == DriveState.Deployed) + + obj.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying) + val reply1 = receiveOne(Duration.create(100, "ms")) + reply1 match { + case Deployment.CanNotChangeDeployment(_, DriveState.Undeploying, _) => () + case _ => assert(false, "") + } + assert(obj.DeploymentState == DriveState.Mobile) obj.Actor ! Deployment.TryDeploy(DriveState.Undeploying) - val reply = receiveOne(Duration.create(100, "ms")) - assert(reply.isInstanceOf[Deployment.CanNotChangeDeployment]) - assert(reply.asInstanceOf[Deployment.CanNotChangeDeployment].obj == obj) - assert(reply.asInstanceOf[Deployment.CanNotChangeDeployment].to_state == DriveState.Undeploying) - assert(obj.DeploymentState == DriveState.Deployed) + val reply2 = receiveOne(Duration.create(100, "ms")) + reply2 match { + case Deployment.CanNotChangeDeployment(_, DriveState.Undeploying, _) => () + case _ => assert(false, "") + } + assert(obj.DeploymentState == DriveState.Mobile) } } } @@ -235,14 +245,24 @@ class DeploymentBehavior6Test extends ActorTest { "Deployment" should { "not undeploy to a deploy state" in { val obj = DeploymentTest.SetUpAgent - assert(obj.DeploymentState == DriveState.Mobile) + obj.DeploymentState = DriveState.Deployed + assert(obj.DeploymentState == DriveState.Deployed) + + obj.Actor ! Deployment.TryDeploymentChange(DriveState.Deploying) + val reply1 = receiveOne(Duration.create(100, "ms")) + reply1 match { + case Deployment.CanNotChangeDeployment(_, DriveState.Deploying, _) => () + case _ => assert(false, "") + } + assert(obj.DeploymentState == DriveState.Deployed) obj.Actor ! Deployment.TryUndeploy(DriveState.Deploying) - val reply = receiveOne(Duration.create(100, "ms")) - assert(reply.isInstanceOf[Deployment.CanNotChangeDeployment]) - assert(reply.asInstanceOf[Deployment.CanNotChangeDeployment].obj == obj) - assert(reply.asInstanceOf[Deployment.CanNotChangeDeployment].to_state == DriveState.Deploying) - assert(obj.DeploymentState == DriveState.Mobile) + val reply2 = receiveOne(Duration.create(100, "ms")) + reply2 match { + case Deployment.CanNotChangeDeployment(_, DriveState.Deploying, _) => () + case _ => assert(false, "") + } + assert(obj.DeploymentState == DriveState.Deployed) } } } diff --git a/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala index 23d2765e1..19bd9eeba 100644 --- a/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala +++ b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala @@ -16,9 +16,9 @@ import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, Plan import scala.concurrent.duration._ class InteractsWithZoneEnvironmentTest extends ActorTest { - val pool1: Pool = Pool(EnvironmentAttribute.Water, DeepSquare(5, 10, 10, 0, 0)) - val pool2: Pool = Pool(EnvironmentAttribute.Water, DeepSquare(5, 10, 15, 5, 10)) - val pool3: Pool = Pool(EnvironmentAttribute.Lava, DeepSquare(5, 15, 10, 10, 5)) + val pool1: Pool = Pool(EnvironmentAttribute.Water, DeepSquare(3, 2, 2, 0, 0)) + val pool2: Pool = Pool(EnvironmentAttribute.Water, DeepSquare(3, 4, 2, 2, 0)) + val pool3: Pool = Pool(EnvironmentAttribute.Lava, DeepSquare(3, 2, 4, 0, 2)) val zoneEvents: TestProbe = TestProbe() val testZone: Zone = { val testMap = new ZoneMap(name = "test-map") { @@ -38,27 +38,26 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val obj = InteractsWithZoneEnvironmentTest.testObject() obj.Zone = testZone obj.Actor = testProbe.ref - obj.Position = Vector3(0,0,50) + obj.Position = Vector3(10,10,0) obj.zoneInteractions() testProbe.expectNoMessage(max = 500 milliseconds) } - "acknowledge interaction when moved into the critical region of a registered environment object (just once)" in { + "acknowledge interaction when moved into the critical region of a registered environment object" in { val testProbe = TestProbe() val obj = InteractsWithZoneEnvironmentTest.testObject() obj.Zone = testZone obj.Actor = testProbe.ref - obj.Position = Vector3(1,1,2) + obj.Position = Vector3(1, 1, 2.7f) obj.zoneInteractions() - val msg = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) + val msg = testProbe.receiveOne(4.seconds) + msg match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, s"$msg") + } + obj.zoneInteractions() testProbe.expectNoMessage(max = 500 milliseconds) } @@ -69,54 +68,48 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { obj.Zone = testZone obj.Actor = testProbe.ref - obj.Position = Vector3(1,1,2) + obj.Position = Vector3(1, 1, 2.7f) obj.zoneInteractions() - val msg1 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg1 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) - - obj.Position = Vector3(1,1,50) + val msg1 = testProbe.receiveOne(4.seconds) + msg1 match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + + obj.Position = Vector3(1,1,5) obj.zoneInteractions() - val msg2 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg2 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) + val msg2 = testProbe.receiveOne(4.seconds) + msg2 match { + case RespondsToZoneEnvironment.StopTimer(EnvironmentAttribute.Water) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + obj.zoneInteractions() testProbe.expectNoMessage(max = 500 milliseconds) } - "transition between two different critical regions when the regions that the same attribute" in { + "transition between two different critical regions when the regions have the same attribute" in { val testProbe = TestProbe() val obj = InteractsWithZoneEnvironmentTest.testObject() obj.Zone = testZone obj.Actor = testProbe.ref - obj.Position = Vector3(7,7,2) + obj.Position = Vector3(1, 1, 2.7f) obj.zoneInteractions() - val msg1 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg1 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) - - obj.Position = Vector3(12,7,2) + val msg1 = testProbe.receiveOne(4.seconds) + msg1 match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + + obj.Position = Vector3(1, 3, 2.7f) obj.zoneInteractions() - val msg2 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg2 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) + val msg2 = testProbe.receiveOne(4.seconds) + msg2 match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => assert(false, "") + } + testProbe.expectNoMessage() } "transition between two different critical regions when the regions have different attributes" in { @@ -125,37 +118,33 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { obj.Zone = testZone obj.Actor = testProbe.ref - obj.Position = Vector3(7,7,2) + obj.Position = Vector3(1, 1, 2.7f) obj.zoneInteractions() - val msg1 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg1 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) - - obj.Position = Vector3(7,12,2) + val msg1 = testProbe.receiveOne(4.seconds) + msg1 match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + + obj.Position = Vector3(3, 1, 2.7f) obj.zoneInteractions() - val msgs = testProbe.receiveN(3, max = 250 milliseconds) - assert( - msgs.head match { - case Vitality.Damage(_) => true - case _ => false - } - ) - assert( - msgs(1) match { - case AuraEffectBehavior.StartEffect(Aura.Fire, _) => true - case _ => false - } - ) - assert( - msgs(2) match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Lava, _, _, _) => true - case _ => false - } - ) + val msgs = testProbe.receiveN(4, 4.seconds) + msgs.head match { + case RespondsToZoneEnvironment.StopTimer(EnvironmentAttribute.Water) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + msgs(1) match { + case Vitality.Damage(_) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + msgs(2) match { + case AuraEffectBehavior.StartEffect(Aura.Fire, _) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + msgs(3) match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Lava, _, _, _) => () + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } } } @@ -165,24 +154,21 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { obj.Zone = testZone obj.Actor = testProbe.ref - obj.Position = Vector3(1,1,2) + obj.Position = Vector3(1, 1, 2.7f) obj.zoneInteractions() val msg1 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg1 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) + msg1 match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } obj.allowInteraction = false val msg2 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg2 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) + msg2 match { + case RespondsToZoneEnvironment.StopTimer(EnvironmentAttribute.Water) => true + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } + obj.zoneInteractions() testProbe.expectNoMessage(max = 500 milliseconds) } @@ -194,22 +180,22 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { obj.Actor = testProbe.ref obj.allowInteraction = false - obj.Position = Vector3(1,1,2) + obj.Position = Vector3(1, 1, 2.7f) obj.zoneInteractions() testProbe.expectNoMessage(max = 500 milliseconds) obj.allowInteraction = true val msg1 = testProbe.receiveOne(max = 250 milliseconds) - assert( - msg1 match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true - case _ => false - } - ) + msg1 match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => true + case _ => assert(InteractsWithZoneEnvironmentTest.fail, "") + } } } object InteractsWithZoneEnvironmentTest { + val fail: Boolean = false + def testObject(): PlanetSideServerObject with InteractsWithZone = { val p = new Player(Avatar(1, "test", PlanetSideEmpire.VS, CharacterSex.Male, 1, CharacterVoice.Mute)) p.GUID = PlanetSideGUID(1) diff --git a/src/test/scala/objects/ResourceSiloTest.scala b/src/test/scala/objects/ResourceSiloTest.scala index 401bdc7ff..aa56739af 100644 --- a/src/test/scala/objects/ResourceSiloTest.scala +++ b/src/test/scala/objects/ResourceSiloTest.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package objects -import akka.actor.Props +import akka.actor.{ActorRef, Props} import akka.testkit.TestProbe import base.{ActorTest, FreedContextActorTest} import net.psforever.actors.zone.BuildingActor @@ -63,7 +63,7 @@ class ResourceSiloTest extends Specification { PlanetSideGUID(0), PlanetSideGUID(2), 0L, - false, + unk3 = false, Vector3(0f, 0f, 0f), Vector3(0f, 0f, 0f), 0, @@ -77,11 +77,11 @@ class ResourceSiloTest extends Specification { } class ResourceSiloControlStartupTest extends ActorTest { - val obj = ResourceSilo() + val obj: ResourceSilo = ResourceSilo() obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") val zone = new Zone("nowhere", new ZoneMap("nowhere-map"), 0) - val buildingEvents = TestProbe("test-building-events") + val buildingEvents: TestProbe = TestProbe("test-building-events") obj.Owner = new Building("Building", building_guid = 6, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) { Actor = buildingEvents.ref @@ -97,11 +97,11 @@ class ResourceSiloControlStartupTest extends ActorTest { } class ResourceSiloControlStartupMessageNoneTest extends ActorTest { - val obj = ResourceSilo() + val obj: ResourceSilo = ResourceSilo() obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") val zone = new Zone("nowhere", new ZoneMap("nowhere-map"), 0) - val buildingEvents = TestProbe("test-building-events") + val buildingEvents: TestProbe = TestProbe("test-building-events") obj.Owner = new Building("Building", building_guid = 6, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) { Actor = buildingEvents.ref @@ -123,11 +123,11 @@ class ResourceSiloControlStartupMessageNoneTest extends ActorTest { } class ResourceSiloControlStartupMessageSomeTest extends ActorTest { - val obj = ResourceSilo() + val obj: ResourceSilo = ResourceSilo() obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") val zone = new Zone("nowhere", new ZoneMap("nowhere-map"), 0) - val buildingEvents = TestProbe("test-building-events") + val buildingEvents: TestProbe = TestProbe("test-building-events") obj.Owner = new Building("Building", building_guid = 6, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) { Actor = buildingEvents.ref @@ -154,19 +154,19 @@ class ResourceSiloControlUseTest extends FreedContextActorTest { expectNoMessage(1000 milliseconds) var buildingMap = new TrieMap[Int, Building]() val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) - val ant = Vehicle(GlobalDefinitions.ant) + val player: Player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + val ant: Vehicle = Vehicle(GlobalDefinitions.ant) val silo = new ResourceSilo() - val catchall = new TestProbe(system).ref - val zone = new Zone("test", new ZoneMap("test-map"), 0) { - override def SetupNumberPools() = {} + val catchall: ActorRef = new TestProbe(system).ref + val zone: Zone = new Zone("test", new ZoneMap("test-map"), 0) { + override def SetupNumberPools(): Unit = {} GUID(guid) - override def AvatarEvents = catchall - override def LocalEvents = catchall - override def VehicleEvents = catchall - override def Activity = catchall - override def Vehicles = List(ant) - override def Buildings = { buildingMap.toMap } + override def AvatarEvents: ActorRef = catchall + override def LocalEvents: ActorRef = catchall + override def VehicleEvents: ActorRef = catchall + override def Activity: ActorRef = catchall + override def Vehicles: List[Vehicle] = List(ant) + override def Buildings: Map[Int, Building] = { buildingMap.toMap } } val building = new Building( name = "integ-fac-test-building", @@ -186,7 +186,7 @@ class ResourceSiloControlUseTest extends FreedContextActorTest { guid.register(silo, number = 5) guid.register(building, number = 6) - val maxNtuCap = ant.Definition.MaxNtuCapacitor + val maxNtuCap: Float = ant.Definition.MaxNtuCapacitor player.Spawn() ant.NtuCapacitor = maxNtuCap val probe = new TestProbe(system) @@ -201,19 +201,18 @@ class ResourceSiloControlUseTest extends FreedContextActorTest { "Resource silo" should { "respond when being used" in { expectNoMessage(1 seconds) - silo.Actor ! CommonMessages.Use(ResourceSiloTest.player) - - val reply = probe.receiveOne(2000 milliseconds) - assert(reply match { - case TransferBehavior.Discharging(Ntu.Nanites) => true - case _ => false - }) + silo.Actor ! CommonMessages.Use(ResourceSiloTest.player, Some(ant)) + val reply = probe.receiveOne(3000 milliseconds) + reply match { + case TransferBehavior.Discharging(Ntu.Nanites) => () + case _ => assert(ResourceSiloTest.fail, "") + } } } } class ResourceSiloControlNtuWarningTest extends ActorTest { - val obj = ResourceSilo() + val obj: ResourceSilo = ResourceSilo() obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") val zone = new Zone("nowhere", new ZoneMap("nowhere-map"), 0) @@ -223,7 +222,7 @@ class ResourceSiloControlNtuWarningTest extends ActorTest { } obj.Owner.GUID = PlanetSideGUID(6) - val zoneEvents = TestProbe("zone-events") + val zoneEvents: TestProbe = TestProbe("zone-events") zone.AvatarEvents = zoneEvents.ref obj.Actor ! Service.Startup() obj.Actor ! ResourceSilo.UpdateChargeLevel(-obj.NtuCapacitor) @@ -237,7 +236,7 @@ class ResourceSiloControlNtuWarningTest extends ActorTest { val reply = zoneEvents.receiveOne(5000 milliseconds) reply match { case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(6), 47, 0)) => ; - case _ => assert(false, s"$reply is wrong") + case _ => assert(ResourceSiloTest.fail, s"$reply is wrong") } assert(!obj.LowNtuWarningOn) } @@ -245,7 +244,7 @@ class ResourceSiloControlNtuWarningTest extends ActorTest { } class ResourceSiloControlUpdate1Test extends ActorTest { - val obj = ResourceSilo() + val obj: ResourceSilo = ResourceSilo() obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") val zone = new Zone("nowhere", new ZoneMap("nowhere-map"), 0) @@ -253,8 +252,8 @@ class ResourceSiloControlUpdate1Test extends ActorTest { new Building("Building", building_guid = 6, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) bldg.GUID = PlanetSideGUID(6) obj.Owner = bldg - val zoneEvents = TestProbe("zone-events") - val buildingEvents = TestProbe("building-events") + val zoneEvents: TestProbe = TestProbe("zone-events") + val buildingEvents: TestProbe = TestProbe("building-events") zone.AvatarEvents = zoneEvents.ref bldg.Actor = buildingEvents.ref obj.Actor ! Service.Startup() @@ -276,12 +275,12 @@ class ResourceSiloControlUpdate1Test extends ActorTest { assert(obj.CapacitorDisplay == 3) reply1.head match { case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(1), 45, 3)) => ; - case _ => assert(false, s"$reply1 is wrong") + case _ => assert(ResourceSiloTest.fail, s"$reply1 is wrong") } assert(reply2.isInstanceOf[BuildingActor.MapUpdate], s"$reply2 is wrong") reply1(1) match { case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(6), 47, 0)) => ; - case _ => assert(false, s"${reply1(1)} is wrong") + case _ => assert(ResourceSiloTest.fail, s"${reply1(1)} is wrong") } assert(!obj.LowNtuWarningOn) } @@ -289,7 +288,7 @@ class ResourceSiloControlUpdate1Test extends ActorTest { } class ResourceSiloControlUpdate2Test extends ActorTest { - val obj = ResourceSilo() + val obj: ResourceSilo = ResourceSilo() obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") val zone = new Zone("nowhere", new ZoneMap("nowhere-map"), 0) @@ -297,8 +296,8 @@ class ResourceSiloControlUpdate2Test extends ActorTest { new Building("Building", building_guid = 6, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) bldg.GUID = PlanetSideGUID(6) obj.Owner = bldg - val zoneEvents = TestProbe("zone-events") - val buildingEvents = TestProbe("building-events") + val zoneEvents: TestProbe = TestProbe("zone-events") + val buildingEvents: TestProbe = TestProbe("building-events") zone.AvatarEvents = zoneEvents.ref bldg.Actor = buildingEvents.ref obj.Actor ! Service.Startup() @@ -320,12 +319,12 @@ class ResourceSiloControlUpdate2Test extends ActorTest { assert(obj.CapacitorDisplay == 2) reply1.head match { case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(1), 45, 2)) => ; - case _ => assert(false, s"$reply1 is wrong") + case _ => assert(ResourceSiloTest.fail, s"$reply1 is wrong") } assert(reply2.isInstanceOf[BuildingActor.MapUpdate]) reply1(1) match { case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(6), 47, 0)) => ; - case _ => assert(false, s"${reply1(1)} is wrong") + case _ => assert(ResourceSiloTest.fail, s"${reply1(1)} is wrong") } assert(!obj.LowNtuWarningOn) } @@ -333,7 +332,7 @@ class ResourceSiloControlUpdate2Test extends ActorTest { } class ResourceSiloControlNoUpdateTest extends ActorTest { - val obj = ResourceSilo() + val obj: ResourceSilo = ResourceSilo() obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") val zone = new Zone("nowhere", new ZoneMap("nowhere-map"), 0) @@ -341,8 +340,8 @@ class ResourceSiloControlNoUpdateTest extends ActorTest { new Building("Building", building_guid = 6, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) bldg.GUID = PlanetSideGUID(6) obj.Owner = bldg - val zoneEvents = TestProbe("zone-events") - val buildingEvents = TestProbe("building-events") + val zoneEvents: TestProbe = TestProbe("zone-events") + val buildingEvents: TestProbe = TestProbe("building-events") zone.AvatarEvents = zoneEvents.ref bldg.Actor = buildingEvents.ref obj.Actor ! Service.Startup() @@ -371,7 +370,9 @@ class ResourceSiloControlNoUpdateTest extends ActorTest { } object ResourceSiloTest { - val player = Player( + val player: Player = Player( Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute).copy(stamina = 0) ) + + val fail: Boolean = false } diff --git a/src/test/scala/objects/TelepadRouterTest.scala b/src/test/scala/objects/TelepadRouterTest.scala index 8c52c5ae0..7eff27c37 100644 --- a/src/test/scala/objects/TelepadRouterTest.scala +++ b/src/test/scala/objects/TelepadRouterTest.scala @@ -15,7 +15,6 @@ import net.psforever.objects.vehicles.{Utility, UtilityType} 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} @@ -253,7 +252,7 @@ class TelepadDeployableResponseFromRouterTest extends FreedContextActorTest { val deploymentProbe = new TestProbe(system) router.Actor.tell(Deployment.TryDeploy(DriveState.Deploying), deploymentProbe.ref) eventsProbe.receiveN(10, 10.seconds) //flush all messages related to deployment - deploymentProbe.receiveOne(2.seconds) //CanDeploy + deploymentProbe.receiveN(2, 10.seconds) //CanDeploy deploymentProbe.expectNoMessage(2.seconds) //intentional delay assert(internal.Active, "link to router test - router internals not active when expected") assert(!telepad.Active, "link to router test - telepad active earlier than intended (2)") diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala index b756d25e0..9b9b7ca79 100644 --- a/src/test/scala/objects/VehicleControlTest.scala +++ b/src/test/scala/objects/VehicleControlTest.scala @@ -14,17 +14,17 @@ import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.environment._ -import net.psforever.objects.serverobject.environment.interaction.{EscapeFromEnvironment, InteractingWithEnvironment, RespondsToZoneEnvironment} +import net.psforever.objects.serverobject.environment.interaction.{InteractingWithEnvironment, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.sourcing.VehicleSource import net.psforever.objects.vehicles.VehicleLockState import net.psforever.objects.vehicles.control.VehicleControl +import net.psforever.objects.vehicles.interaction.WithWater import net.psforever.objects.vital.{ShieldCharge, SpawningActivity, Vitality} import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game._ import net.psforever.services.ServiceManager -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types._ @@ -174,15 +174,17 @@ class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { //} class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActorTest { - val vehicleProbe = new TestProbe(system) - val catchall = new TestProbe(system) + val eventsProbe = new TestProbe(system) + val cargoProbe = new TestProbe(system) val guid = new NumberPoolHub(new MaxNumberSource(10)) ServiceManager.boot val zone = new Zone("test", new ZoneMap("test"), 0) { GUID(guid) override def SetupNumberPools(): Unit = {} - override def VehicleEvents = vehicleProbe.ref + override def AvatarEvents = eventsProbe.ref + override def LocalEvents = eventsProbe.ref + override def VehicleEvents = eventsProbe.ref } zone.actor = system.spawn(ZoneActor(zone), "test-zone-actor") // crappy workaround but without it the zone doesn't get initialized in time @@ -191,7 +193,6 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) vehicle.Faction = PlanetSideEmpire.TR vehicle.Zone = zone - val cargoProbe = new TestProbe(system) vehicle.Actor = cargoProbe.ref val lodestar = Vehicle(GlobalDefinitions.lodestar) lodestar.Faction = PlanetSideEmpire.TR @@ -220,39 +221,45 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor "if with mounted cargo, eject it when marked for deconstruction" in { lodestar.Actor ! Vehicle.Deconstruct() - val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) - vehicle_msg.head match { - case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(4), 4, true, PlanetSideGUID(2))) => ; + val vehicleMsgs = eventsProbe.receiveN(6, 10.seconds) + val cargoMsgs = cargoProbe.receiveN(1, 1.seconds) + vehicleMsgs.head match { + case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(4), 4, true, PlanetSideGUID(2))) => () case _ => - assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-1: ${vehicle_msg(5)}") + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-1: ${vehicleMsgs.head}") } assert(player2.VehicleSeated.isEmpty) assert(lodestar.Seats(0).occupant.isEmpty) //cargo dismounting messages - vehicle_msg(1) match { - case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => ; + vehicleMsgs(1) match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => () case _ => - assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-2: ${vehicle_msg.head}") + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-2: ${vehicleMsgs(1)}") } - vehicle_msg(2) match { - case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => ; + vehicleMsgs(2) match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => () case _ => - assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-3: ${vehicle_msg(1)}") + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-3: ${vehicleMsgs(2)}") } - vehicle_msg(3) match { + vehicleMsgs(3) match { case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0))) => ; case _ => - assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-4: ${vehicle_msg(2)}") + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-4: ${vehicleMsgs(3)}") + } + vehicleMsgs(4) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _))) => () + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-5: ${vehicleMsgs(4)}") } - vehicle_msg(4) match { - case VehicleServiceMessage("test", VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _))) => ; + vehicleMsgs(5) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0))) => () case _ => - assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-5: ${vehicle_msg(3)}") + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-6: ${vehicleMsgs(5)}") } - vehicle_msg(5) match { - case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0))) => ; + cargoMsgs.head match { + case Vehicle.Deconstruct(_) => () case _ => - assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-6: ${vehicle_msg(4)}") + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-7: ${cargoMsgs.head}") } } } @@ -393,7 +400,7 @@ class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { val player1 = Player(VehicleTest.avatar1) player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar1) + val player2 = Player(VehicleTest.avatar1.copy(basic = VehicleTest.avatar1.basic.copy(faction = PlanetSideEmpire.NC))) player2.GUID = PlanetSideGUID(2) "Vehicle Control" should { @@ -578,13 +585,13 @@ class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest { // } //} -class VehicleControlInteractWithWaterPartialTest extends ActorTest { - val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 +class VehicleControlInteractWithWaterWadingTest extends ActorTest { val playerProbe = TestProbe() + val vehicleProbe = TestProbe() + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val guid = new NumberPoolHub(new MaxNumberSource(15)) - val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(5, 2, 2, 0, 0)) val zone = new Zone( id = "test-zone", new ZoneMap(name = "test-map") { @@ -609,36 +616,35 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest { vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID player1.Actor = playerProbe.ref - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") + vehicle.Actor = vehicleProbe.ref "VehicleControl" should { - "causes disability when the vehicle drives too deep in water (check driver messaging)" in { - vehicle.Position = Vector3(5,5,-3) //right in the pool - vehicle.zoneInteractions() //trigger + "report when the vehicle starts treading water" in { + vehicle.Position = Vector3(1, 1, 6) + vehicle.zoneInteractions() + vehicleProbe.expectNoMessage(2.seconds) + playerProbe.expectNoMessage() - val msg_drown = playerProbe.receiveOne(250 milliseconds) - assert( - msg_drown match { - case InteractingWithEnvironment( - p2, - Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f)) - ) => (p2 eq pool) - case _ => false - } - ) + vehicle.Position = Vector3(1, 1, 4f) + vehicle.zoneInteractions() + val vehicleMsgs = vehicleProbe.receiveN(1, 5.seconds) + vehicleMsgs.head match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => + assert(false, "") + } + playerProbe.expectNoMessage() } } } -class VehicleControlInteractWithWaterTest extends ActorTest { - val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 - val avatarProbe = TestProbe() +class VehicleControlInteractWithWaterStartDrowningTest extends ActorTest { val playerProbe = TestProbe() val vehicleProbe = TestProbe() + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val guid = new NumberPoolHub(new MaxNumberSource(15)) - val pool = Pool(EnvironmentAttribute.Water, DeepSquare(10, 10, 10, 0, 0)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(5, 2, 2, 0, 0)) val zone = new Zone( id = "test-zone", new ZoneMap(name = "test-map") { @@ -650,53 +656,155 @@ class VehicleControlInteractWithWaterTest extends ActorTest { GUID(guid) override def LivePlayers = List(player1) override def Vehicles = List(vehicle) - override def AvatarEvents = avatarProbe.ref - override def VehicleEvents = avatarProbe.ref - - this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command] } zone.blockMap.addTo(vehicle) zone.blockMap.addTo(pool) guid.register(player1, 1) guid.register(vehicle, 2) - guid.register(player1.avatar.locker, 5) player1.Zone = zone player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID - val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) player1.Actor = playerProbe.ref vehicle.Actor = vehicleProbe.ref "VehicleControl" should { - "causes disability when the vehicle drives too deep in water" in { - vehicle.Position = Vector3(5,5,3) //right in the pool - vehicle.zoneInteractions() //trigger + "report when the vehicle starts drowning" in { + vehicle.Position = Vector3(1, 1, 6) + vehicle.zoneInteractions() + vehicleProbe.expectNoMessage(2.seconds) + playerProbe.expectNoMessage() - val msg_drown = playerProbe.receiveOne(250 milliseconds) - assert(msg_drown match { - case InteractingWithEnvironment(body, _) => body eq pool - case _ => false - }) - val msg_disable = vehicleProbe.receiveOne(10 seconds) - assert(msg_disable match { - case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, VehicleControl.Disable(true)) => true - case _ => false - }) + vehicle.Position = Vector3(1, 1, 0f) + vehicle.zoneInteractions() + val vehicleMsgs = vehicleProbe.receiveN(3, 5.seconds) + val playerMsgs = playerProbe.receiveN(1, 1.seconds) + vehicleMsgs.head match { + case RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) => () + case _ => + assert(false, "") + } + vehicleMsgs(1) match { + case RespondsToZoneEnvironment.Timer(WithWater.WaterAction, _, _, _) => () + case _ => + assert(false, "") + } + vehicleMsgs(2) match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => + assert(false, "") + } + playerMsgs.head match { + case InteractingWithEnvironment(somePool, Some(OxygenStateTarget(ValidPlanetSideGUID(2), _, OxygenState.Suffocation, 100.0f))) + if somePool eq pool => () + case _ => + assert(false, "") + } } } } -class VehicleControlStopInteractWithWaterTest extends ActorTest { - val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 +//class VehicleControlInteractWithWaterStopDrowningTest extends ActorTest { +// val playerProbe = TestProbe() +// val vehicleProbe = TestProbe() +// val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 +// val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 +// val guid = new NumberPoolHub(new MaxNumberSource(15)) +// val pool = Pool(EnvironmentAttribute.Water, DeepSquare(5, 2, 2, 0, 0)) +// val zone = new Zone( +// id = "test-zone", +// new ZoneMap(name = "test-map") { +// environment = List(pool) +// }, +// zoneNumber = 0 +// ) { +// override def SetupNumberPools() = {} +// GUID(guid) +// override def LivePlayers = List(player1) +// override def Vehicles = List(vehicle) +// } +// zone.blockMap.addTo(vehicle) +// zone.blockMap.addTo(pool) +// +// guid.register(player1, 1) +// guid.register(vehicle, 2) +// player1.Zone = zone +// player1.Spawn() +// vehicle.Zone = zone +// vehicle.Faction = PlanetSideEmpire.TR +// vehicle.Seats(0).mount(player1) +// player1.VehicleSeated = vehicle.GUID +// player1.Actor = playerProbe.ref +// vehicle.Actor = vehicleProbe.ref +// +// "VehicleControl" should { +// "report when the vehicle stops drowning" in { +// vehicle.Position = Vector3(1, 1, 6) +// vehicle.zoneInteractions() +// vehicleProbe.expectNoMessage(2.seconds) +// playerProbe.expectNoMessage() +// +// vehicle.Position = Vector3(1, 1, 0f) +// vehicle.zoneInteractions() +// val vehicleMsgs = vehicleProbe.receiveN(3, 5.seconds) +// val playerMsgs = playerProbe.receiveN(1, 1.seconds) +// vehicleMsgs.head match { +// case RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) => () +// case _ => +// assert(false, "") +// } +// vehicleMsgs(1) match { +// case RespondsToZoneEnvironment.Timer(WithWater.WaterAction, _, _, _) => () +// case _ => +// assert(false, "") +// } +// vehicleMsgs(2) match { +// case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () +// case _ => +// assert(false, "") +// } +// playerMsgs.head match { +// case InteractingWithEnvironment(somePool, Some(OxygenStateTarget(ValidPlanetSideGUID(2), _, OxygenState.Suffocation, 100.0f))) +// if somePool eq pool => () +// case _ => +// assert(false, "") +// } +// +// //escape drowning +// vehicle.Position = Vector3(1, 1, 4.7f) +// vehicle.zoneInteractions() +// val vehicleMsgs2 = vehicleProbe.receiveN(2, 5.seconds) +// val playerMsgs2 = playerProbe.receiveN(1, 1.seconds) +// vehicleMsgs2.head match { +// case RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) => () +// case _ => +// assert(false, "") +// } +// vehicleMsgs2(1) match { +// case RespondsToZoneEnvironment.Timer(WithWater.WaterAction, _, _, _) => () +// case _ => +// assert(false, "") +// } +// playerMsgs2.head match { +// case EscapeFromEnvironment(somePool, Some(OxygenStateTarget(ValidPlanetSideGUID(2), _, OxygenState.Recovery, _))) +// if somePool eq pool => () +// case _ => +// assert(false, "") +// } +// } +// } +//} + +class VehicleControlInteractWithWaterStopWadingTest extends ActorTest { val playerProbe = TestProbe() + val vehicleProbe = TestProbe() + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val guid = new NumberPoolHub(new MaxNumberSource(15)) - val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(5, 2, 2, 0, 0)) val zone = new Zone( id = "test-zone", new ZoneMap(name = "test-map") { @@ -721,39 +829,130 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest { vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID player1.Actor = playerProbe.ref - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") + vehicle.Actor = vehicleProbe.ref "VehicleControl" should { - "stop becoming disabled if the vehicle drives out of the water" in { - vehicle.Position = Vector3(5,5,-3) //right in the pool - vehicle.zoneInteractions() //trigger - val msg_drown = playerProbe.receiveOne(250 milliseconds) - assert( - msg_drown match { - case InteractingWithEnvironment( - p2, - Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f)) - ) => (p2 eq pool) - case _ => false - } - ) + "report when the vehicle stops wading" in { + vehicle.Position = Vector3(1, 1, 6) + vehicle.zoneInteractions() + vehicleProbe.expectNoMessage(2.seconds) + playerProbe.expectNoMessage() - vehicle.Position = Vector3.Zero //that's enough of that + vehicle.Position = Vector3(1, 1, 4f) vehicle.zoneInteractions() - val msg_recover = playerProbe.receiveOne(250 milliseconds) - assert( - msg_recover match { - case EscapeFromEnvironment( - p2, - Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Recovery, _)) - ) => (p2 eq pool) - case _ => false - } - ) + val vehicleMsgs = vehicleProbe.receiveN(1, 5.seconds) + playerProbe.expectNoMessage() + vehicleMsgs.head match { + case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () + case _ => + assert(false, "") + } + + //stop wading + vehicle.Position = Vector3(1, 1, 6f) + vehicle.zoneInteractions() + val vehicleMsgs2 = vehicleProbe.receiveN(1, 5.seconds) + playerProbe.expectNoMessage() + vehicleMsgs2.head match { + case RespondsToZoneEnvironment.StopTimer(EnvironmentAttribute.Water) => () + case _ => + assert(false, "") + } } } } +//class VehicleControlInteractWithWaterFullStopTest extends ActorTest { +// val playerProbe = TestProbe() +// val vehicleProbe = TestProbe() +// val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 +// val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 +// val guid = new NumberPoolHub(new MaxNumberSource(15)) +// val pool = Pool(EnvironmentAttribute.Water, DeepSquare(5, 2, 2, 0, 0)) +// val zone = new Zone( +// id = "test-zone", +// new ZoneMap(name = "test-map") { +// environment = List(pool) +// }, +// zoneNumber = 0 +// ) { +// override def SetupNumberPools() = {} +// GUID(guid) +// override def LivePlayers = List(player1) +// override def Vehicles = List(vehicle) +// } +// zone.blockMap.addTo(vehicle) +// zone.blockMap.addTo(pool) +// +// guid.register(player1, 1) +// guid.register(vehicle, 2) +// player1.Zone = zone +// player1.Spawn() +// vehicle.Zone = zone +// vehicle.Faction = PlanetSideEmpire.TR +// vehicle.Seats(0).mount(player1) +// player1.VehicleSeated = vehicle.GUID +// player1.Actor = playerProbe.ref +// vehicle.Actor = vehicleProbe.ref +// +// "VehicleControl" should { +// "report when the vehicle stops interacting with water altogether" in { +// vehicle.Position = Vector3(1, 1, 6) +// vehicle.zoneInteractions() +// vehicleProbe.expectNoMessage(2.seconds) +// playerProbe.expectNoMessage() +// //wading and drowning +// vehicle.Position = Vector3(1, 1, 0f) +// vehicle.zoneInteractions() +// val vehicleMsgs = vehicleProbe.receiveN(3, 5.seconds) +// val playerMsgs = playerProbe.receiveN(1, 1.seconds) +// vehicleMsgs.head match { +// case RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) => () +// case _ => +// assert(false, "") +// } +// vehicleMsgs(1) match { +// case RespondsToZoneEnvironment.Timer(WithWater.WaterAction, _, _, _) => () +// case _ => +// assert(false, "") +// } +// vehicleMsgs(2) match { +// case RespondsToZoneEnvironment.Timer(EnvironmentAttribute.Water, _, _, _) => () +// case _ => +// assert(false, "") +// } +// playerMsgs.head match { +// case InteractingWithEnvironment(somePool, Some(OxygenStateTarget(ValidPlanetSideGUID(2), _, OxygenState.Suffocation, 100.0f))) +// if somePool eq pool => () +// case _ => +// assert(false, "") +// } +// +// //escape drowning and wading +// vehicle.Position = Vector3(1, 1, 6f) +// vehicle.zoneInteractions() +// val vehicleMsgs2 = vehicleProbe.receiveN(2, 5.seconds) +// val playerMsgs2 = playerProbe.receiveN(1, 1.seconds) +// vehicleMsgs2.head match { +// case RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) => () +// case _ => +// assert(false, "") +// } +// vehicleMsgs2(1) match { +// case RespondsToZoneEnvironment.StopTimer(EnvironmentAttribute.Water) => () +// case _ => +// assert(false, "") +// } +// playerMsgs2.head match { +// case EscapeFromEnvironment(somePool, Some(OxygenStateTarget(ValidPlanetSideGUID(2), _, OxygenState.Recovery, _))) +// if somePool eq pool => () +// case _ => +// assert(false, "") +// } +// } +// } +//} + class VehicleControlInteractWithLavaTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 val player1 =