diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 9afecac87..3aabbf12e 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -11,7 +11,7 @@ import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.ContinentalLockUpdateMessage import net.psforever.persistence -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.{SendResponse, SetEmpire} import net.psforever.services.galaxy.GalaxyAction import net.psforever.services.{InterstellarClusterService, ServiceManager} @@ -232,8 +232,10 @@ class BuildingActor( Behaviors.same case MapUpdate() => - details.galaxyService ! MessageEnvelope("", GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) - details.galaxyService ! MessageEnvelope("", SendResponse(details.building.densityLevelUpdateMessage(building))) + details.galaxyService ! BundledEnvelope( + MessageEnvelope("", GalaxyAction.MapUpdate(details.building.infoUpdateMessage())), + MessageEnvelope("", SendResponse(details.building.densityLevelUpdateMessage(building))) + ) Behaviors.same case AmenityStateChange(amenity, data) => diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala index 1045dcb23..ba3f8bdc3 100644 --- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala @@ -15,7 +15,7 @@ import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, Ca import net.psforever.objects.sourcing.PlayerSource import net.psforever.packet.game.{GenericObjectActionMessage, PlanetsideAttributeMessage} import net.psforever.services.InterstellarClusterService -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.{GenericObjectAction, PlanetsideAttribute, SendResponse} import net.psforever.services.galaxy.GalaxyAction import net.psforever.services.local.support.{CaptureEnvelope, HackCaptureActor, HackClearActor, HackClearEnvelope} @@ -204,25 +204,19 @@ case object MajorFacilityLogic val events = zone.AvatarEvents val guid = building.GUID val msg = GenericObjectAction(guid, 15) - building.PlayersInSOI.foreach { player => - events ! MessageEnvelope(player.Name, msg) - } + events ! BundledEnvelope(building.PlayersInSOI.map { player => MessageEnvelope(player.Name, msg) }) false case Some(GeneratorControl.Event.Critical) => val events = zone.AvatarEvents val guid = building.GUID val msg = PlanetsideAttribute(guid, 46, 1) - building.PlayersInSOI.foreach { player => - events ! MessageEnvelope(player.Name, msg) - } + events ! BundledEnvelope(building.PlayersInSOI.map { player => MessageEnvelope(player.Name, msg) }) true case Some(GeneratorControl.Event.Destabilized) => val events = zone.AvatarEvents val guid = building.GUID val msg = GenericObjectAction(guid, 16) - building.PlayersInSOI.foreach { player => - events ! MessageEnvelope(player.Name, msg) - } + events ! BundledEnvelope(building.PlayersInSOI.map { player => MessageEnvelope(player.Name, msg) }) if (building.hasCavernLockBenefit) { zone.LocalEvents ! MessageEnvelope( zone.id, @@ -235,10 +229,9 @@ case object MajorFacilityLogic case Some(GeneratorControl.Event.Offline) => powerLost(details) val zone = building.Zone + val events = zone.AvatarEvents val msg = PlanetsideAttribute(building.GUID, 46, 2) - building.PlayersInSOI.foreach { player => - zone.AvatarEvents ! MessageEnvelope(player.Name, msg) - } //??? + events ! BundledEnvelope(building.PlayersInSOI.map { player => MessageEnvelope(player.Name, msg) }) //??? true case Some(GeneratorControl.Event.Normal) => true @@ -253,9 +246,7 @@ case object MajorFacilityLogic PlanetsideAttributeMessage(guid, 46, 0), GenericObjectActionMessage(guid, 17) )) - building.PlayersInSOI.foreach { player => - events ! MessageEnvelope(player.Name, list) - } + events ! BundledEnvelope(building.PlayersInSOI.map { player => MessageEnvelope(player.Name, list) }) true case _ => false diff --git a/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala b/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala index 2553a30fd..ec60d2f8d 100644 --- a/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala @@ -6,7 +6,7 @@ import akka.actor.typed.scaladsl.Behaviors import net.psforever.actors.commands.NtuCommand import net.psforever.actors.zone.BuildingActor import net.psforever.objects.serverobject.structures.{Amenity, Building, WarpGate} -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.galaxy.GalaxyAction import net.psforever.types.PlanetSideEmpire import net.psforever.util.Config @@ -209,9 +209,7 @@ case object WarpGateLogic warpgate.Zone.Number, warpgate.MapId, previousAllowances, setBroadcastTo ) warpgate.AllowBroadcastFor = setBroadcastTo - (setBroadcastTo ++ previousAllowances).foreach { faction => - events ! MessageEnvelope(faction.toString, msg) - } + events ! BundledEnvelope((setBroadcastTo ++ previousAllowances).map { faction => MessageEnvelope(faction.toString, msg) }) } /** diff --git a/src/main/scala/net/psforever/objects/BoomerDeployable.scala b/src/main/scala/net/psforever/objects/BoomerDeployable.scala index 84285db2c..22ce1938d 100644 --- a/src/main/scala/net/psforever/objects/BoomerDeployable.scala +++ b/src/main/scala/net/psforever/objects/BoomerDeployable.scala @@ -107,10 +107,7 @@ class BoomerDeployableControl(mine: BoomerDeployable) zone.Ground ! Zone.Ground.RemoveItem(guid) case _ => () } - zone.AvatarEvents! MessageEnvelope( - zone.id, - ObjectDelete(guid) - ) + zone.AvatarEvents! MessageEnvelope(zone.id, ObjectDelete(guid)) TaskWorkflow.execute(GUIDTask.unregisterObject(zone.GUID, trigger)) case None => () } diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala index 30203cc70..7aa3fee0d 100644 --- a/src/main/scala/net/psforever/objects/Players.scala +++ b/src/main/scala/net/psforever/objects/Players.scala @@ -20,11 +20,12 @@ import net.psforever.objects.zones.Zone import net.psforever.packet.game._ import net.psforever.types.{ChatMessageType, ExoSuitType, PlanetSideGUID, Vector3} import net.psforever.services.avatar.AvatarAction -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.{ObjectDelete, SendResponse} import net.psforever.services.local.LocalAction import scala.annotation.tailrec +import scala.collection.mutable.ArrayBuffer object Players { private val log = org.log4s.getLogger("Players") @@ -49,10 +50,7 @@ object Players { ) { val events = target.Zone.AvatarEvents val uname = user.Name - events ! MessageEnvelope( - uname, - SendResponse(RepairMessage(target.GUID, progress.toInt)) - ) + events ! MessageEnvelope(uname, SendResponse(RepairMessage(target.GUID, progress.toInt))) true } else { false @@ -439,24 +437,20 @@ object Players { if ((player.Slot(index).Equipment = obj).contains(obj)) { val fireMode = tool.FireModeIndex val ammoType = tool.AmmoTypeIndex + val list: ArrayBuffer[MessageEnvelope] = ArrayBuffer() player.Inventory -= x.start obj.FireModeIndex = fireMode //TODO any penalty for being handed an OCM version of the tool? - events ! MessageEnvelope( - zone.id, - AvatarAction.EquipmentInHand(pguid, index, obj) - ) + list.append(MessageEnvelope(zone.id, AvatarAction.EquipmentInHand(pguid, index, obj))) if (obj.AmmoTypeIndex != ammoType) { obj.AmmoTypeIndex = ammoType - events ! MessageEnvelope( - name, - SendResponse(ChangeAmmoMessage(obj.GUID, ammoType)) - ) + list.append(MessageEnvelope(name, SendResponse(ChangeAmmoMessage(obj.GUID, ammoType)))) } if (player.DrawnSlot == Player.HandsDownSlot) { player.DrawnSlot = index - events ! MessageEnvelope(zone.id, pguid, AvatarAction.ObjectHeld(index, index)) + list.append(MessageEnvelope(zone.id, pguid, AvatarAction.ObjectHeld(index, index))) } + events ! BundledEnvelope(list) } case Nil => ; //no replacements found } diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index 5cf4abbe8..2ef24b137 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -12,11 +12,13 @@ import net.psforever.objects.vehicles._ import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ChatMsg, FrameVehicleStateMessage, GenericObjectActionEnum, HackMessage, HackState, HackState1, HackState7, TriggeredSound, VehicleStateMessage} import net.psforever.types.{ChatMessageType, DriveState, PlanetSideEmpire, PlanetSideGUID, Vector3} -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.{GenericObjectAction, PlanetsideAttribute, SendResponse, SetEmpire} import net.psforever.services.local.LocalAction import net.psforever.services.vehicle.VehicleAction +import scala.collection.mutable.ArrayBuffer + //import scala.concurrent.duration._ object Vehicles { @@ -248,13 +250,15 @@ object Vehicles { val hFaction = hacker.Faction val zone = target.Zone val zoneid = zone.id + val avatarEvents = zone.AvatarEvents val vehicleEvents = zone.VehicleEvents - val localEvents = zone.LocalEvents val previousOwnerName = target.OwnerName.getOrElse("") - vehicleEvents ! MessageEnvelope( + val occupantMessages: ArrayBuffer[MessageEnvelope] = ArrayBuffer() + val vehicleMessages: ArrayBuffer[MessageEnvelope] = ArrayBuffer() + vehicleMessages.append(MessageEnvelope( zoneid, SendResponse(HackMessage(HackState1.Unk2, tGuid, hGuid, 100, 0f, HackState.Hacked, HackState7.Unk8)) - ) + )) target.Actor ! CommonMessages.Hack(hacker, target) // Forcefully dismount any cargo target.CargoHolds.foreach { case (_, cargoHold) => @@ -268,26 +272,29 @@ object Vehicles { player: Player => seat.unmount(player) player.VehicleSeated = None - vehicleEvents ! MessageEnvelope( + occupantMessages.append(MessageEnvelope( zoneid, player.GUID, VehicleAction.KickPassenger(4, unk2 = false, tGuid) - ) + )) } // In case BFR is occupied and may or may not be crouched if (GlobalDefinitions.isBattleFrameVehicle(target.Definition) && target.Seat(0).isDefined) { - zone.LocalEvents ! MessageEnvelope( - zoneid, - GenericObjectAction(target.GUID, GenericObjectActionEnum.BFRShieldsDown.id) - ) - zone.LocalEvents ! MessageEnvelope( - zoneid, - SendResponse( - FrameVehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), unk2=false, 0, 0, is_crouched=true, is_airborne=false, ascending_flight=false, 10, 0, 0))) - zone.LocalEvents ! MessageEnvelope( - zoneid, - SendResponse( - VehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), None, 0, 0, 15, is_decelerating=false, is_cloaked=false))) + vehicleMessages.appendAll(List( + MessageEnvelope(zoneid, + GenericObjectAction(target.GUID, GenericObjectActionEnum.BFRShieldsDown.id) + ), + MessageEnvelope(zoneid, + SendResponse( + FrameVehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), unk2=false, 0, 0, is_crouched=true, is_airborne=false, ascending_flight=false, 10, 0, 0) + ) + ), + MessageEnvelope(zoneid, + SendResponse( + VehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), None, 0, 0, 15, is_decelerating=false, is_cloaked=false) + ) + ) + )) } }) // If the vehicle can fly and is flying: deconstruct it; and well played to whomever managed to hack a plane in mid air @@ -312,26 +319,17 @@ object Vehicles { Vehicles.Own(target, hacker) //todo: Send HackMessage -> HackCleared to vehicle? can be found in packet captures. Not sure if necessary. // And broadcast the faction change to other clients - zone.AvatarEvents ! MessageEnvelope( - zoneid, - SetEmpire(tGuid, hFaction) - ) + vehicleMessages.append(MessageEnvelope(zoneid, SetEmpire(tGuid, hFaction))) } - localEvents ! MessageEnvelope( + vehicleMessages.append(MessageEnvelope( zoneid, hGuid, LocalAction.TriggerSound(TriggeredSound.HackVehicle, target.Position, 30, 0.49803925f) - ) + )) if (zone.Players.exists(_.name.equals(previousOwnerName))) { - localEvents ! MessageEnvelope( - previousOwnerName, - SendResponse(ChatMsg(ChatMessageType.UNK_226, "@JackStolen")) - ) + vehicleMessages.append(MessageEnvelope(previousOwnerName, SendResponse(ChatMsg(ChatMessageType.UNK_226, "@JackStolen")))) } - localEvents ! MessageEnvelope( - hacker.Name, - SendResponse(ChatMsg(ChatMessageType.UNK_226, "@JackVehicleOwned")) - ) + occupantMessages.append(MessageEnvelope(hacker.Name, 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 { @@ -343,13 +341,15 @@ object Vehicles { util.Actor ! TelepadLike.Activate(util) } case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed => - vehicleEvents ! MessageEnvelope(zone.id, VehicleAction.AMSDeploymentChange(zone)) + vehicleMessages.append(MessageEnvelope(zone.id, VehicleAction.AMSDeploymentChange(zone))) case _ => () } - vehicleEvents ! MessageEnvelope( + vehicleMessages.append(MessageEnvelope( zoneid, SendResponse(HackMessage(HackState1.Unk2, tGuid, tGuid, 0, 1L, HackState.HackCleared, HackState7.Unk8)) - ) + )) + avatarEvents ! BundledEnvelope(occupantMessages) + vehicleEvents ! BundledEnvelope(vehicleMessages) target.Actor ! CommonMessages.ClearHack() } diff --git a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala index 7100c7580..160776650 100644 --- a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala +++ b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala @@ -6,7 +6,7 @@ import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects._ import net.psforever.objects.zones.Zone import net.psforever.packet.game._ -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.SetEmpire import net.psforever.services.local.LocalAction import net.psforever.types.PlanetSideEmpire @@ -198,11 +198,14 @@ trait DeployableBehavior { None } //zone build - localEvents ! MessageEnvelope(zone.id, LocalAction.DeployItem(obj)) - //zone map icon - localEvents ! MessageEnvelope( - obj.Faction.toString, - LocalAction.DeployableMapIcon(DeploymentAction.Build, DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.OwnerGuid.getOrElse(Default.GUID0))) + localEvents ! BundledEnvelope( + /* zone build */ + MessageEnvelope(zone.id, LocalAction.DeployItem(obj)), + /* zone map icon */ + MessageEnvelope( + obj.Faction.toString, + LocalAction.DeployableMapIcon(DeploymentAction.Build, DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.OwnerGuid.getOrElse(Default.GUID0))) + ) ) //local build management callback ! Zone.Deployable.IsBuilt(obj) @@ -281,15 +284,13 @@ object DeployableBehavior { val localEvents = zone.LocalEvents if (originalFaction != toFaction) { obj.Faction = toFaction - //visual tells in regards to ownership by faction - zone.AvatarEvents ! MessageEnvelope( - zone.id, - SetEmpire(dGuid, toFaction) - ) - //remove knowledge by the previous owner's faction - localEvents ! MessageEnvelope( - originalFaction.toString, - LocalAction.DeployableMapIcon(DeploymentAction.Dismiss, info) + localEvents ! BundledEnvelope( + /* visual tells in regards to ownership by faction */ + MessageEnvelope(zone.id, SetEmpire(dGuid, toFaction)), + /* remove knowledge by the previous owner's faction */ + MessageEnvelope(originalFaction.toString, LocalAction.DeployableMapIcon(DeploymentAction.Dismiss, info)), + /* display to the given faction */ + MessageEnvelope(toFaction.toString, LocalAction.DeployableMapIcon(DeploymentAction.Build, info)) ) //remove deployable from original owner's toolbox and UI counter zone.AllPlayers.filter(p => obj.OriginalOwnerName.contains(p.Name)) @@ -297,11 +298,6 @@ object DeployableBehavior { originalOwner.avatar.deployables.Remove(obj) originalOwner.Zone.LocalEvents ! MessageEnvelope(originalOwner.Name, LocalAction.DeployableUIFor(obj.Definition.Item)) } - //display to the given faction - localEvents ! MessageEnvelope( - toFaction.toString, - LocalAction.DeployableMapIcon(DeploymentAction.Build, info) - ) } } } diff --git a/src/main/scala/net/psforever/objects/ce/TelepadLike.scala b/src/main/scala/net/psforever/objects/ce/TelepadLike.scala index 3ac01cfb9..f90273a38 100644 --- a/src/main/scala/net/psforever/objects/ce/TelepadLike.scala +++ b/src/main/scala/net/psforever/objects/ce/TelepadLike.scala @@ -9,7 +9,7 @@ import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.zones.Zone import net.psforever.packet.game.{GenericObjectActionMessage, ObjectCreateMessage, ObjectDeleteMessage} import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.SendResponse import net.psforever.services.local.LocalAction import net.psforever.types.PlanetSideGUID @@ -115,21 +115,23 @@ object TelepadLike { normally dispatched while the Router is transitioned into its Deploying state it is safe, however, to perform these actions at any time during and after the Deploying state */ - events ! MessageEnvelope( - zoneId, - SendResponse( - ObjectCreateMessage( - udef.ObjectId, - utilityGUID, - ObjectCreateMessageParent(routerGUID, 2), //TODO stop assuming slot number - udef.Packet.ConstructorData(obj).get + events ! BundledEnvelope( + MessageEnvelope( + zoneId, + SendResponse( + ObjectCreateMessage( + udef.ObjectId, + utilityGUID, + ObjectCreateMessageParent(routerGUID, 2), //TODO stop assuming slot number + udef.Packet.ConstructorData(obj).get + ) ) - ) + ), + MessageEnvelope(zoneId, SendResponse(Seq( + GenericObjectActionMessage(utilityGUID, 27), + GenericObjectActionMessage(utilityGUID, 30) + ))) ) - events ! MessageEnvelope(zoneId, SendResponse(Seq( - GenericObjectActionMessage(utilityGUID, 27), - GenericObjectActionMessage(utilityGUID, 30) - ))) LinkTelepad(zone, utilityGUID) } diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala index 10a94effb..55b1b17be 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala @@ -7,7 +7,7 @@ import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vital.interaction.{Adversarial, DamageResult} import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity, SpawningActivity} import net.psforever.services.avatar.AvatarAction -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.types.PlanetSideEmpire import net.psforever.util.Config @@ -52,9 +52,9 @@ object KillAssists { history: Iterable[InGameActivity], eventBus: ActorRef ): Unit = { - rewardThisPlayerDeath(victim, lastDamage, history).foreach { case (p, kda) => - eventBus ! MessageEnvelope(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda)) - } + eventBus ! BundledEnvelope(rewardThisPlayerDeath(victim, lastDamage, history).map { case (p, kda) => + MessageEnvelope(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda)) + }) } /** diff --git a/src/main/scala/net/psforever/services/base/GenericEventService.scala b/src/main/scala/net/psforever/services/base/GenericEventService.scala index 183daf018..ac098ca68 100644 --- a/src/main/scala/net/psforever/services/base/GenericEventService.scala +++ b/src/main/scala/net/psforever/services/base/GenericEventService.scala @@ -4,7 +4,7 @@ package net.psforever.services.base import akka.actor.Actor import net.psforever.services.Service import net.psforever.services.base.bus.GenericEventBus -import net.psforever.services.base.envelope.GenericMessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, GenericMessageEnvelope} import org.log4s.Logger /** @@ -78,6 +78,9 @@ class GenericEventService(stamp: EventSystemStamp) * Accept and handle designated messages. */ protected def commonBehavior: Receive = { + case bundle: BundledEnvelope => + bundle.msgs.foreach(commonBehavior.apply) + case msg: GenericMessageEnvelope => handleMessage(msg) } diff --git a/src/main/scala/net/psforever/services/base/GenericEventServiceWithCacheAndSupport.scala b/src/main/scala/net/psforever/services/base/GenericEventServiceWithCacheAndSupport.scala index 47b34e8d8..1f284af76 100644 --- a/src/main/scala/net/psforever/services/base/GenericEventServiceWithCacheAndSupport.scala +++ b/src/main/scala/net/psforever/services/base/GenericEventServiceWithCacheAndSupport.scala @@ -3,7 +3,7 @@ package net.psforever.services.base import akka.actor.Cancellable import net.psforever.objects.Default -import net.psforever.services.base.envelope.{GenericMessageEnvelope, GenericResponseEnvelope, MessageEnvelope, MessageTransformationBehavior} +import net.psforever.services.base.envelope.{BundledEnvelope, GenericMessageEnvelope, GenericResponseEnvelope, MessageEnvelope, MessageTransformationBehavior} import net.psforever.services.base.message.EventMessage import net.psforever.types.PlanetSideGUID import net.psforever.util.Config @@ -137,18 +137,35 @@ class GenericEventServiceWithCacheAndSupport } /** - * Add messages to the cache based on their channel, then their type, then their cache target identifier. + * Add messages to the cache. + * @param event event system message + */ + private def pushToCache(event: CachedGenericEventEnvelope): Unit = { + pushToCache(event, event.msg.getClass.getName, event.guid) + } + /** + * Add messages to the cache. + * @param event event system message + * @param eventGuid event system message filter + */ + private def pushToCache(event: GenericMessageEnvelope, eventGuid: PlanetSideGUID): Unit = { + pushToCache(event, event.msg.getClass.getName, eventGuid) + } + /** + * Add messages to the cache + * based on their channel, then their type, then their cache target identifier. * Messages that arrive with the same cache profile as a previous message, * but before that previous message was dispatched, * will overwrite the previous message without fanfare or warning. * @param event event system message + * @param eventClassName event system message identifier + * @param eventGuid event system message filter */ - private def pushToCache(event: CachedGenericEventEnvelope): Unit = { - val eventClassName = event.msg.getClass.getName + private def pushToCache(event: GenericMessageEnvelope, eventClassName: String, eventGuid: PlanetSideGUID): Unit = { val updateBranch = cache .getOrElseUpdate(event.channel, new ConcurrentHashMap[String, CMap[PlanetSideGUID, GenericMessageEnvelope]]().asScala) .getOrElseUpdate(eventClassName, new ConcurrentHashMap[PlanetSideGUID, GenericMessageEnvelope]().asScala) - updateBranch.updateWith(event.guid) { _ => Some(event) } + updateBranch.updateWith(eventGuid) { _ => Some(event) } tryRetimeFlushCache() } @@ -194,6 +211,8 @@ class GenericEventServiceWithCacheAndSupport event match { case FlushCachedMessages => flushCache() + case bundle: BundledEnvelope => + handleMessageBundled(bundle) case _: CachedGenericEventEnvelope if tryFlushCache() => super.handleMessage(event) case envelope: CachedGenericEventEnvelope => @@ -203,4 +222,24 @@ class GenericEventServiceWithCacheAndSupport super.handleMessage(event) } } + + /** + * If even one message in a bundle is to be cached, the contents of the whole bundle should be cached. + * Otherwise, just handle things normally. + * @param bundle event system message that may be cached + */ + private def handleMessageBundled(bundle: BundledEnvelope): Unit = { + val messages = bundle.msgs + messages.find(_.isInstanceOf[CachedGenericEventEnvelope]) match { + case Some(cache: CachedGenericEventEnvelope) if messages.size == 1 => + pushToCache(messages.head, cache.guid) + tryFlushCache() + case Some(cache: CachedGenericEventEnvelope) => + val guid = cache.guid + messages.foreach(msg => pushToCache(msg, guid)) + tryFlushCache() + case _ => + messages.foreach(handleMessage) + } + } } diff --git a/src/main/scala/net/psforever/services/base/envelope/BundledEnvelope.scala b/src/main/scala/net/psforever/services/base/envelope/BundledEnvelope.scala new file mode 100644 index 000000000..3c90c8fb8 --- /dev/null +++ b/src/main/scala/net/psforever/services/base/envelope/BundledEnvelope.scala @@ -0,0 +1,83 @@ +// Copyright (c) 2026 PSForever +package net.psforever.services.base.envelope + +import net.psforever.objects.Default +import net.psforever.services.base.EventSystemStamp +import net.psforever.services.base.message.{EventMessage, EventResponse} +import net.psforever.types.PlanetSideGUID + +import scala.annotation.tailrec + +/** + * A message when there should be none, but a message is required to be defined anyway. + */ +case object NoMessage extends EventMessage { + override def response(): EventResponse = NoReply +} + +/** + * A response envelope when there should be none, but an envelope for messaging product is required to be defined anyway. + */ +case object NoResponse extends GenericResponseEnvelope { + override def reply: EventResponse = NoReply + override def stamp: EventSystemStamp = Undelivered + override def channel: String = "" + override def filter: PlanetSideGUID = Default.GUID0 +} + +/** + * A collection of messaging envelopes to be dispatched to an event system at the same time + * within the conditions of event system synchronization between different messages. + * All messages contained within the bundling are to be processed at the time of processing by the bundling. + * The order of the bundled message envelopes should be considered important. + * @see `SendResponse(Seq[PlanetSideGamePacket])` + * @param msgs list of message envelopes + */ +final case class BundledEnvelope(msgs: Iterable[GenericMessageEnvelope]) + extends GenericMessageEnvelope { + assert(msgs.size == BundledEnvelope.unwind(msgs).size, "do not nest bundled event system envelopes") + + override def msg: EventMessage = NoMessage + override def response(stamp: EventSystemStamp): GenericResponseEnvelope = NoResponse + override def channel: String = "" + override def filter: PlanetSideGUID = Default.GUID0 +} + +object BundledEnvelope { + /** + * Overloaded constructor for `BundledEnvelope` objects. + * The entities are separated between "the first" and "any others" to distinguish from + * the `case class` constructor that accepts any number of message envelopes + * including no message envelopes at all. + * @param first single, required `GenericMessageEnvelope` entity for bundling + * @param msgs any other `GenericMessageEnvelope` entity for bundling + * @return a `BundledEnvelope` object + */ + def apply(first: GenericMessageEnvelope, msgs: GenericMessageEnvelope*): BundledEnvelope = { + new BundledEnvelope(unwind(first +: msgs)) + } + + /** + * Input sanitization method that unpacks `BundledEnvelope` message envelopes + * producing a single-dimensional list of `GenericMessageEnvelope` entities. + * An assertion in the constructor of `BundledEnvelope` aborts object creation + * if a `BundledEnvelope` entity is within that `BundledEnvelope` entity. + * @param in list of `GenericMessageEnvelope` entities to be processed + * @param out list of `GenericMessageEnvelope` entities that have been processed + * @return list of `GenericMessageEnvelope` entities that have been processed + */ + @tailrec + private def unwind(in: Iterable[GenericMessageEnvelope], out: List[GenericMessageEnvelope] = Nil): List[GenericMessageEnvelope] = { + if (in.isEmpty) { + out + } else { + val first :: remainder = in + first match { + case bundle: BundledEnvelope => + unwind(bundle.msgs ++ remainder, out) + case _ => + unwind(remainder, out :+ first) + } + } + } +} diff --git a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala index a8a189a86..e3ca9a6e4 100644 --- a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala +++ b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala @@ -6,7 +6,7 @@ import net.psforever.objects.{Default, Doors} import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.zones.Zone import net.psforever.services.base.{EventServiceSupport, GenericSupportEnvelope} -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.local.LocalAction.IsADoorMessage import net.psforever.services.local.LocalAction import net.psforever.types.PlanetSideGUID @@ -64,11 +64,13 @@ class DoorCloseActor() extends Actor { doorsLeftOpen1 ++ doorsLeftOpen2.map(entry => DoorCloseActor.DoorEntry(entry.door, entry.zone, now)) ).sortBy(_.time) - doorsToClose2.foreach { case DoorCloseActor.DoorEntry(door, zone, _) => - door.Open = None //permissible break from synchronization - zone.LocalEvents ! MessageEnvelope(zone.id, LocalAction.DoorCloses(door.GUID)) //call up to the main event system - } - + doorsToClose2 + .map { case DoorCloseActor.DoorEntry(door, zone, _) => + door.Open = None //permissible break from synchronization + (zone, MessageEnvelope(zone.id, LocalAction.DoorCloses(door.GUID))) //call up to the main event system + } + .groupBy(_._1) + .foreach { case (zone, list) => zone.LocalEvents ! BundledEnvelope(list.map(_._2)) } if (openDoors.nonEmpty) { val short_timeout: FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)).milliseconds import scala.concurrent.ExecutionContext.Implicits.global 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 f0af48a72..61c27ec6f 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -14,7 +14,7 @@ import net.psforever.objects.serverobject.structures.participation.MajorFacility import net.psforever.packet.game.{ChatMsg, GenericAction, HackState7, PlanetsideAttributeEnum} import net.psforever.objects.sourcing.PlayerSource import net.psforever.services.base.{EventServiceSupport, GenericSupportEnvelopeOnly} -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.PlanetsideAttribute import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction import net.psforever.services.local.LocalAction @@ -22,6 +22,7 @@ import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID} import java.util.concurrent.{Executors, TimeUnit} import scala.collection.Seq +import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration.{FiniteDuration, _} import scala.util.Random @@ -277,18 +278,18 @@ class HackCaptureActor extends Actor { private def HackCompleted(terminal: CaptureTerminal with Hackable, hackedByFaction: PlanetSideEmpire.Value): Unit = { val building = terminal.Owner.asInstanceOf[Building] + val zone = building.Zone + val events = zone.LocalEvents + val messages: ArrayBuffer[MessageEnvelope] = ArrayBuffer() if (building.NtuLevel > 0) { building.virusId = 8 building.virusInstalledBy = None log.info(s"Setting base ${building.GUID} / MapId: ${building.MapId} as owned by $hackedByFaction") //dispatch to players aligned with the capturing faction within the SOI - val events = building.Zone.LocalEvents val msg = LocalAction.GenericActionMessage(GenericAction.FacilityCaptureFanfare) - building - .PlayersInSOI - .collect { case p if p.Faction == hackedByFaction => - events ! MessageEnvelope(p.Name, msg) - } + messages.appendAll(building.PlayersInSOI.collect { case p if p.Faction == hackedByFaction => + MessageEnvelope(p.Name, msg) + }) val buildings = building.Zone.Buildings.values val hackedBaseId = building.GUID val facilities = if (building.Zone.id.startsWith("c")) { @@ -322,8 +323,8 @@ class HackCaptureActor extends Actor { } NotifyHackStateChange(terminal, isResecured = true) // todo: this appears to be the way to reset the base warning lights after the hack finishes but it doesn't seem to work. - val zone = building.Zone - zone.LocalEvents ! MessageEnvelope(zone.id, LocalAction.HackClear(building.GUID, 3212836864L, HackState7.Unk8)) + messages.append(MessageEnvelope(zone.id, LocalAction.HackClear(building.GUID, 3212836864L, HackState7.Unk8))) + events ! BundledEnvelope(messages) } private def RestartTimer(): Unit = { diff --git a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala index 1d7796984..429cb59c3 100644 --- a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala @@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.zones.Zone import net.psforever.packet.game.HackState7 -import net.psforever.services.base.envelope.MessageEnvelope +import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.{EventServiceSupport, GenericSupportEnvelope, GenericSupportEnvelopeOnly} import net.psforever.services.base.message.GenericObjectAction import net.psforever.services.local.LocalAction.IsAHackMessage @@ -72,13 +72,18 @@ class HackClearActor() extends Actor { //TODO we can just walk across the list of doors and extract only the first few entries val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now) hackedObjects = stillHackedObjects - unhackObjects.foreach { case HackClearActor.HackEntry(target, zone, unk1, unk2, _, _) => - target.Actor ! CommonMessages.ClearHack() - zone.LocalEvents ! MessageEnvelope(zone.id, LocalAction.HackClear(target.GUID, unk1, unk2)) - if (target.Definition == GlobalDefinitions.main_terminal) { - ClearVirusFromBuilding(target) + unhackObjects + .map { case HackClearActor.HackEntry(target, zone, unk1, unk2, _, _) => + target.Actor ! CommonMessages.ClearHack() + if (target.Definition == GlobalDefinitions.main_terminal) { + ClearVirusFromBuilding(target) + } + (zone, MessageEnvelope(zone.id, LocalAction.HackClear(target.GUID, unk1, unk2))) + } + .groupBy(_._1) + .foreach { case (zone, list) => + zone.LocalEvents ! BundledEnvelope(list.map(_._2)) } - } RestartTimer() @@ -132,9 +137,7 @@ class HackClearActor() extends Actor { building.virusInstalledBy = None val msg = GenericObjectAction(target.GUID, 60) val events = building.Zone.AvatarEvents - building.PlayersInSOI.foreach { player => - events ! MessageEnvelope(player.Name, msg) - } + events ! BundledEnvelope(building.PlayersInSOI.map { player => MessageEnvelope(player.Name, msg) }) building.Actor ! BuildingActor.MapUpdate() }